Quantcast
Channel: Yet Another Tridion Blog
Viewing all 215 articles
Browse latest View live

Single Sign-On SDL Web 8.5 CME and ADFS

$
0
0
In order to use SDL Web 8.5 and ADFS, we first have to enable SSO (Single Sign-On) on the SDL Web side.

The setup I have is:
  • SDL Web 8.5 installed in AWS (but it is the on-premise version of SDL Web)
  • Active Directory outside of AWS
  • ADFS outside of AWS, but somewhere close to AD
  • HTTPS access to SDL Web CME website
For this setup to work, we need to enable/configure a few things:
  • enable HTTPS access to the CME
  • enable/configure SSO on the CME
  • ADFS SAML Authenticator

Enable HTTPS

In PowerShell, run the script SetupHTTPS.ps1 from folder [SDLWebHome]\bin\Configuration Scripts. A very detailed explanation of this process is available at https://docs.sdl.com/LiveContent/content/en-US/SDL%20Web-v5/GUID-D694CEFB-AE01-415E-B919-5867C08E0A18.

A prerequisite of enabling HTTPS is obtaining and installing a SSL Certificate. This is beyond the scope of this blog post, but the PowerShell script above expects the 40 character certificate thumbprint as a parameter.

After running the PowerShell script, verify in IIS that the SDL CME website has a binding for port 443 (HTTPS) and that the SSL Certificate is configured for that binding.



Enable SSO

In PowerShell, run the script SetupSSO.ps1 from folder [SDLWebHome]\bin\Configuration Scripts. A very detailed explanation of this process is available at https://docs.sdl.com/LiveContent/content/en-US/SDL%20Web-v1/GUID-32378192-5366-4805-85AF-C578F988993B.

The parameter UserNameHeader is an important one, because it defines the name of the request header that the HTTP Module SsoAgentHttpModule is reading in order to impersonate the CME user. This SsoAgentHttpModule is a module that comes out of the box with SDL Web and it is configured and enabled by the PowerShell script.

ADFS SAML Authenticator

This is a custom piece of software that bridges the ADFS server with the SDL SsoAgentHttpModule.

The authenticator performs the following tasks:
  • request a SAML token to the ADFS server
  • intercept POST back with SAML token from ADFS server
  • decrypt token
  • extract user name from token
  • put user name in a request header
The SsoAgentHttpModule can now use the user name from the request header in order to impersonate the CME user.

The ADFS SAML Authenticator is described in more detail, in the following blog post.




SAML Authenticator Web 8.5 with ADFS

$
0
0
This post continues the setup of Single Sign-On for Web 8.5 with ADFS presented in an earlier blog post.

This SAML Authenticator is a .NET HTTP Module that is configured to intercept all requests going into the SDL Web website (the CME) and do the following:
  • if request is post-back from ADFS
    • decrypt SAML token
    • extract user name
    • set cookie with user name
  • else
    • if cookie exists
      • extract user name from cookie
      • set user name in request SSO header
    • else
      • redirect browser to ADFS form-login
The cookie is encrypted, in order to prevent the user name from being spoofed.

In more detail, the code is as follows:

publicvoidBeginRequest()
{
string user = GetUserData();
if (user == null)
{
Request.Headers.Remove(Configuration.HeaderUser);
}
else
{
Request.Headers[Configuration.HeaderUser] = user;
}
}

The method executes when a request is intercepted. The configuration HeaderUser is the name of the SSO header that the SDL Web SsoAgentHttpModule is expecting to be set, in order to impersonate that user.

The method GetUserData:

privatestringGetUserData()
{
string result = null;
SamlResponse samlResponse;

if ((samlResponse = new SamlResponse()).IsValid)
{
UserData user = samlResponse.ExtractUser();

if (user == null)
{
newSamlRequest().ForceLogin();
}
else
{
Response.SetCookie(new HttpCookie(Configuration.CookieName, Util.RsaEncrypt(user.UserId)));

if (string.IsNullOrEmpty(user.OriginalUrl))
{
result = user.UserId;
}
else
{
Response.Redirect(user.OriginalUrl);
}
}
}
else
{
HttpCookie cookie = Request.Cookies[Configuration.CookieName];

if (cookie == null)
{
newSamlRequest().ForceLogin();
}
else
{
result = Util.RsaDecrypt(cookie.Value);
if (result == null)
{
newSamlRequest().ForceLogin();
}
}
}

return result;
}

The classes SamlRequest and SamlResponse handle the request redirect to ADFS login, and response token handling respectively.

SamleResponse.ExtractUser method is presented below:

public UserData ExtractUser()
{
UserData user = new UserData();

AdfsClient adfsClient = new AdfsClient();
Saml2SecurityToken token = adfsClient.DecryptSaml2Token(SamlResponseXml);
IPrincipal principal = adfsClient.GetPrincipal(token);

ExtractClaims(principal, user);

return user;
}

The code above makes use of the custom AdfsClient class, which handles communication and token handling with the ADFS server.

The code first decodes the SAML encrypted XML and converts the string representation of the token into a .NET System.IdentityModel.Tokens.Sam2SecurityToken object.

public Saml2SecurityToken DecryptSaml2Token(string tokenXml)
{
using (StringReader stringReader = new StringReader(tokenXml))
using (XmlReader reader = XmlReader.Create(stringReader))
{
reader.ReadToFollowing("EncryptedAssertion", SAML2_ASSERTION);
return (Saml2SecurityToken)SecurityTokenHandlers.ReadToken(reader);
}
}

public ClaimsPrincipal GetPrincipal(SecurityToken decryptedToken)
{
ReadOnlyCollection<ClaimsIdentity> identities = SecurityTokenHandlers.ValidateToken(decryptedToken);
returnnewClaimsPrincipal(identities[0]);
}

The latter method, GetPrincipal, validates the decrypted token and by doing so, it creates a collection of ClaimsIdentity objects contained in that token. We then return a new ClaimsPrincipal instance based on the first identity in the token.

Lastly, the SamlRequest.ForgeLogin method simply outputs an HTML document that automatically performs a JavaScript POST to the ADFS passive-authentication login-form. It also passes in the Relying Party identifier for this application (without such an identifier, the ADFS won't show the for-login screen).

We must also configure the HTTP Module in file web.config of the SDL Web website, located in folder [SDLWebHome]\web.


CoreService with ADFS

$
0
0
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>
<bindingname="myCoreServiceBinding"maxReceivedMessageSize="10485760">
<securitymode="TransportWithMessageCredential">
<messageissuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
<issueraddress="https://myadfs.com/adfs/services/trust/2005/usernamemixed"
binding="wsHttpBinding"bindingConfiguration="myIssuerBinding"/>
<issuerMetadataaddress="https://myadfs.com/adfs/services/trust/mex"/>

<tokenRequestParameters>
<trust:SecondaryParametersxmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
<trust:KeyTypexmlns: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:KeySizexmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
<trust:KeyWrapAlgorithmxmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
<trust:EncryptWithxmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
<trust:SignWithxmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
<trust:CanonicalizationAlgorithmxmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
<trust:EncryptionAlgorithmxmlns: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>
<bindingname="myIssuerBinding"transactionFlow="true">
<securitymode="TransportWithMessageCredential">
<transportclientCredentialType="None"/>
<messageclientCredentialType="UserName"establishSecurityContext="false"/>
</security>
</binding>
</wsHttpBinding>
</bindings>

<client>
<endpointaddress="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>
<claimsAuthenticationManagertype="My.ClaimsAuthenticationManager"/>

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

<issuerNameRegistrytype="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<trustedIssuers>
<addname="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>

<certificateValidationcertificateValidationMode="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>
<bindingname="CoreService_wsFederationHttpBinding"transactionFlow="true"maxReceivedMessageSize="10485760">
<securitymode="TransportWithMessageCredential">

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

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

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

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

<behaviors>
<serviceBehaviors>
<behaviorname="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior">
<serviceCredentialsuseIdentityConfiguration="true">
<serviceCertificatestoreLocation="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>

<serviceAuthorizationprincipalPermissionMode="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:

publicclassMyClaimsAuthenticationManager : ClaimsAuthenticationManager
{
publicoverride ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
ClaimsPrincipal user = HttpContext.Current.User as ClaimsPrincipal;

if (user != null&& user.Identity.IsAuthenticated && user.Identity.AuthenticationType == "Federation")
{
returnnewClaimsPrincipal(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.



CoreService and ADFS with Issued Token

$
0
0
If in an earlier post CoreService with ADFS, I was talking about XML configuration of the .NET client application when connecting to a SDL Web 8.5 instance secured with ADFS, in this post I am showing another way of connecting to the CoreService, namely using a SAML token requested through code rather than configuration.

The main steps in this approach are:
  • client .NET Console application requests programmatically a SAML token from the ADFS server
  • client creates connection using issued token
The setup on the SDL Web server and the configurations of the CoreService web-service are identical to those presented in the earlier post, therefore I won't mention them again here.

App.Config

The client is a .NET Console application using an App.config which defines the following CoreService endpoint:

<system.serviceModel>
<bindings>
<ws2007FederationHttpBinding>
<bindingname="myCoreServiceBinding"maxReceivedMessageSize="10485760">
<securitymode="TransportWithMessageCredential">
<messageissuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
<issueraddress="http://some.url"binding="basicHttpBinding"/>
</message>
</security>
</binding>
</ws2007FederationHttpBinding>
</bindings>

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

Note that the security is setup as TransportWithMessageCredential, which means HTTPS containing some message that contains the user credential. Furthermore, the format of the message is defined as SAML v2. The issuer of the SAML token is set to a fictitious address, but it has to be specified, otherwise we get System.IdentityModel.Selectors.CardSpaceException.

The client uses generated CoreService proxy classes and this helps also in creating the client endpoint above.

Client Code

The client makes 2 separate calls when creating a channel with the CoreService:
  • get SAML token from ADFS server
  • open connection to CoreService using issued token
The method GetToken() calls the Security Toke Service (STS) endpoint on the ADFS server, i.e. https://myadfs.com/adfs/services/trust/2005/usernamemixed, passes in the username, password and Relying Party identifier, and requests a SAML2 token. If username / password are correct, then ADFS issues an encrypted SAML token.

public SecurityToken GetToken()
{
WSHttpBinding binding = new WSHttpBinding(SecurityMode.TransportWithMessageCredential);
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
binding.Security.Message.EstablishSecurityContext = false;

var endpoint = new EndpointAddress(stsUrl);

using (var factory = new WSTrustChannelFactory(binding, endpoint))
{
factory.Credentials.UserName.UserName = username;
factory.Credentials.UserName.Password = password;
IWSTrustChannelContract channel = factory.CreateChannel();

RequestSecurityToken request = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointReference(audienceId),
TokenType = "urn:oasis:names:tc:SAML:2.0:assertion"
};

return channel.Issue(request);
}
}

The client won't decrypt the token, but rather will send it further to the CoreService when creating the connection. The token is only decrypted on the SDL Web server.

In the code below a ChannelFactory is created around the ISessionAwareCoreService generated proxy. Then we create an actual channel to the CoreService by passing the SAML token to the ChannelFactory in method CreateChannelWithIssuedToken.

The server decrypts the token, extracts the user in it and creates a security context for that user. During the established session, the operations with the web service happen in the name of the impersonated user.

SecurityToken token = GetToken();

using (var factory = new ChannelFactory<ISessionAwareCoreService>("coreServiceFederation"))
{
factory.Credentials.UseIdentityConfiguration = true;
ISessionAwareCoreService coreService = factory.CreateChannelWithIssuedToken(token);

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

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



Basic Authenticator Web 8.5 with ADFS

$
0
0
This post presents a Basic Authenticator wrapper around ADFS security. This authenticator allows a client and web-service to use Basic authorization security, while the actual security authentication happens on an ADFS server.

The Basic Authenticator is a .NET HTTP module, which creates a security context based on a user principal that it receives from ADFS. All subsequent request processing happens in the context of this 'impersonated' context.

The request processing sequence is as follows:
  1. Client connects to web-service using Basic authorization (over HTTPS, so the Authorization request header is not transmitted in the open)
  2. Basic Authenticator HTTP module intercepts the request
  3. If Authorization header is in the request
    1. Extract and decode username/password from Authorization header
    2. If username/password is valid in ADFS
      1. Create user principal
      2. Set security context
      3. Let request processing continue
    3. Else
      1. Send 401 WWW-Authenticate response header
      2. Stop request processing
  4. Else
    1. Send 401 WWW-Authenticate response header
    2. Stop request processing

Request processing starts with AuthenticateRequest method. If there is an Authorization header, then try extract and validate user with ADFS; otherwise, prepare to return 401 response.

publicvoidAuthenticateRequest()
{
var authorization = Request.Headers["Authorization"];
if (authorization != null)
{
string credentials = GetCredentials(authorization);
if (credentials != null&& AuthenticateUser(credentials))
{
Request.Headers[Configuration.HeaderUser] = HttpContext.Current.User.Identity.Name;

return;
}
}

HttpContext.Current.Response.StatusCode = 401;
}

The request processing ends with method EndRequest. If the response code was set to 401, then send a WWW-Authenticate header also to force the client to send a request header Authorization. Unless the request type is OPTIONS (which means it is a WebDAV request), in which case we send back 200 OK.

publicvoidEndRequest()
{
if (Response.StatusCode == 401)
{
if (Request.RequestType == "OPTIONS")
{
Response.StatusCode = 200;
}
else
{
Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", Realm));
}
}
}

Method GetCredentials decodes the Authorization header and extracts the username/password:

privatestringGetCredentials(string authorizationHeader)
{
var headerValue = AuthenticationHeaderValue.Parse(authorizationHeader);
if (!headerValue.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(headerValue.Parameter))
{
returnnull;
}

var encoding = Encoding.GetEncoding("iso-8859-1");
byte[] credentialsBytes = Convert.FromBase64String(headerValue.Parameter);

return encoding.GetString(credentialsBytes);
}

Next, the AuthenticateUser method is called in case we have a successfully extracted username/password from the Authorization header. This method checks the credentials agains ADFS and sets the security context both in current HttpContext and current Thread objects.

privateboolAuthenticateUser(string credentials)
{
IPrincipal principal = CheckPassword(credentials);
if (principal != null)
{
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;

returntrue;
}

returnfalse;
}

Next, the method CheckPassword is where the credentials are checked against ADFS. If successful, ADFS returns a SAML token, which we need to decrypt, extract the user from it, and then create a user principal for this user:

private ClaimsPrincipal CheckPassword(string credentials)
{
string[] parts = credentials.Split(':');
if (parts.Length != 2)
{
returnnull;
}

AdfsClient adfsClient = new AdfsClient(parts[0], parts[1],
Utils.Configuration.AdfsAudienceId, Utils.Configuration.AdfsStsUrl);
GenericXmlSecurityToken encryptedToken = adfsClient.GetToken();
SecurityToken decryptedToken = adfsClient.DecryptToken(encryptedToken);

return adfsClient.GetPrincipal(decryptedToken);
}

The code above makes use of a custom class AdfsClient that abstracts out the interaction with the ADFS server. The main methods are the following:
  • GetToken: requests a SAML token from the ADFS server, passing in the user, password and Relying Party identifier and returns an XML representation of the token;
  • DecryptToken: decrypts the encrypted SAML token and converts it into a Saml2SecurityToken;
  • GetPrincipal: extracts the ClaimsIdentity from the decrypted SAML token and returns a ClaimsPrincipal;

For brevity, I won't go into details about the AdfsClient class in this blog post. Instead, I will dedicate an entire blog post to AdfsClient shortly.



AdfsClient Class

$
0
0
As mentioned in a previous blog post, this class takes care of the interaction with the ADFS server.

Its main functionality is to request and decrypt SAML tokens from an ADFS server.

The class makes use of the .NET security token API, which in order to be used, it has to be configured. The easiest is to configure it through XML, in one of the application's .config files:

<system.identityModel.services>
<federationConfiguration>
<serviceCertificate>
<certificateReferencestoreLocation="LocalMachine"storeName="My"x509FindType="FindByThumbprint"
findValue="30 4e 10 91 73 fb 34 6a 90 19 f5 e7 d4 fa 2d 11 21 10 3e 3d"/>
</serviceCertificate>
</federationConfiguration>
</system.identityModel.services>

The configuration above allows us to use the predefined System.IdentityModel.Tokens.Saml2SecurityTokenHandler. I used this class to handle the reading (i.e. decrypting) and validating (i.e. extracting the claims) the token. If configured correctly, this class makes the entire token handling quite easy.

The configuration above refers to the SSL Certificate that must be installed on the machine running the AdfsClient, and basically configures a way of looking up the certificate by its thumbprint. This certificate is used to decrypt the encrypted SAML XML token, so it must contain a private key. This same certificate (only the public key) is configured in the ADFS Relying Party encryption panel and it is used by the ADFS server to encrypt the token when it's issued.

publicconststring SAML2_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion";

private SecurityTokenHandlerCollection securityTokenHandlers;
private SecurityTokenHandlerCollection SecurityTokenHandlers
{
get
{
if (securityTokenHandlers == null)
{
securityTokenHandlers = new SecurityTokenHandlerCollection(
FederatedAuthentication.FederationConfiguration.IdentityConfiguration.SecurityTokenHandlers);
for (int i = 0; i < securityTokenHandlers.Count; i++)
{
if (securityTokenHandlers[i] is Saml2SecurityTokenHandler)
{
securityTokenHandlers[i] = new ValidatingSaml2SecurityTokenHandler
{
Configuration = securityTokenHandlers[0].Configuration
};
}
}
}

return securityTokenHandlers;
}
}

publicAdfsClient()
{
IsInitialized = false;
}

publicAdfsClient(string username, string password, string audienceUri, string stsEndpoint)
{
IsInitialized = true;
Username = username;
Password = password;
AudienceUri = audienceUri;
StsEndpoint = stsEndpoint;
}

The constructors above are self explanatory, but they also initialize the current instance.

The method GetToken below, requests a token from the ADFS server, but only if there is no token already present in the internal cache. This is done in order to prevent requesting a new token for every single request. The cache key is constructed based off the user, password hash, audienceUri and STS endpoint.

A token is placed into cache for the duration that token is valid, as specified in the token itself.

public GenericXmlSecurityToken GetToken()
{
if (!IsInitialized)
{
thrownewAdfsClientException("AdfsClinet not initialized");
}

GenericXmlSecurityToken token;

if (!cache.TryGet(CacheKey, out token))
{
WSHttpBinding binding = new WSHttpBinding();
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
binding.Security.Message.EstablishSecurityContext = false;
binding.Security.Mode = SecurityMode.TransportWithMessageCredential;

var endpoint = new EndpointAddress(StsEndpoint);

using (var factory = new WSTrustChannelFactory(binding, endpoint))
{
factory.Credentials.UserName.UserName = Username;
factory.Credentials.UserName.Password = Password;
IWSTrustChannelContract channel = factory.CreateChannel();

RequestSecurityToken request = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointReference(AudienceUri),
TokenType = SAML2_ASSERTION
};

token = (GenericXmlSecurityToken)channel.Issue(request);

cache.Insert(CacheKey, token, token.ValidTo - token.ValidFrom);
}
}

return token;
}

Next, method DecryptSaml2Token takes a string representation of the SAML token, as it was received from the ADFS server, and decodes it into a generic XML token and at the same time decrypts the encrypted part of the XML. This method is used during passive-authentication from the ADFS, namely in the previously explained SamlAuthenticationHandler.

The method uses a StringReader to identify where the token starts in the XML, and then uses the ReadToken method of the SecyrityTokenHandlers collection.

public Saml2SecurityToken DecryptSaml2Token(string tokenXml)
{
using (StringReader stringReader = new StringReader(tokenXml))
using (XmlReader reader = XmlReader.Create(stringReader))
{
reader.ReadToFollowing("EncryptedAssertion", SAML2_ASSERTION);
return (Saml2SecurityToken)SecurityTokenHandlers.ReadToken(reader);
}
}

The method DecryptToken below, decrypts a GenericXmlSecurityToken token and returns a SecurityToken, which represents a specific decrypted security token, but under a generic type. Since we know we are requesting SAML2 tokens from ADFS, it's safe to assume this token is in fact a Saml2SecurityToken that contains claims.

Also this method removes the object from cache, in case the decryption fails.

public SecurityToken DecryptToken(GenericXmlSecurityToken encryptedToken)
{
try
{
var reader = new XmlNodeReader(encryptedToken.TokenXml);
return SecurityTokenHandlers.ReadToken(reader);
}
catch (Exception e)
{
cache.Remove<object>(CacheKey);
thrownewAdfsClientException("Unable to decrypt token", e);
}
}

Next, the method GetPrincipal reads the claims inside the token and extracts them in a collection of ClaimsIdentity objects by calling the ValidateToken method on the SecurityTokenHandlers collection.

It returns a ClaimsPrincipal object based off this claims.

public ClaimsPrincipal GetPrincipal(SecurityToken decryptedToken)
{
try
{
ReadOnlyCollection<ClaimsIdentity> identities = SecurityTokenHandlers.ValidateToken(decryptedToken);
returnnewClaimsPrincipal(identities[0]);
}
catch (Exception e)
{
cache.Remove<object>(CacheKey);
thrownewAdfsClientException("Unable to validate token", e);
}
}




A Validating Saml2SecurityTokenHandler

$
0
0
In a previous blog post, I talked about a SecurityTokenHandlers collection of different security tokens. This is a class from the .NET security framework and it allows a unified entry to different types of tokens.

In this post, I explain the need for a validating Saml2SecurityTokenHandler.

The code for my overloaded property is something like this:

private SecurityTokenHandlerCollection securityTokenHandlers;
private SecurityTokenHandlerCollection SecurityTokenHandlers
{
get
{
if (securityTokenHandlers == null)
{
securityTokenHandlers = new SecurityTokenHandlerCollection(
FederatedAuthentication.FederationConfiguration.IdentityConfiguration.SecurityTokenHandlers);
for (int i = 0; i < securityTokenHandlers.Count; i++)
{
if (securityTokenHandlers[i] is Saml2SecurityTokenHandler)
{
securityTokenHandlers[i] = new ValidatingSaml2SecurityTokenHandler
{
Configuration = securityTokenHandlers[0].Configuration
};
}
}
}

return securityTokenHandlers;
}
}

The property SecurityTokenHandlers above modifies the predefined collection of handlers and inserts my own ValidatingSaml2SecurityTokenHandler. This is required because the ADFS server issues a token that contains a property InResponseTo. The default handler would throw an exception if this property is present in the SAML token, hence the need to implement my own handler with validation. The validating handler makes sure the SAML token we received, is indeed the token we requested (i.e. it matches our request ID).

The class ValidatingSaml2SecurityTokenHandler is a specialized class of Saml2SecurityTokenHandler, which implements the ValidateConfirmationData method. This method is called by the .NET security framework, if the SAML2 token contains a field SubjectId, which contains an InResponseTo value. This extra check is done to prevent DOS attacks. When a SAML2 issue request is made, the request contains an ID that only the client knows. When a token is issued, it contains the same ID in the InResponseTo field. This way, the client can check that the token was indeed issued in response to the request ID that the client initially sent. This logic is not implemented by default in .NET security API, and thus we implement it ourselves.

publicclassValidatingSaml2SecurityTokenHandler : Saml2SecurityTokenHandler
{
protectedoverridevoidValidateConfirmationData(Saml2SubjectConfirmationData confirmationData)
{
Saml2Id saml2Id = confirmationData.InResponseTo;
if (saml2Id == null)
{
return;
}

string key = saml2Id.Value;
HttpCookie cookie = HttpContext.Current.Request.Cookies[key];

if (cookie == null)
{
thrownewException("Original request id not found");
}
}
}

The method above expects a cookie that contains the request ID that was initially sent. If no such cookie is present, we consider the response to be invalid and reject the token.




ImportExport Service with ADFS

$
0
0
Using the ImportExport service with ADFS authentication is quite straight forward, if we use the Basic Authenticator. This one exposes a Basic authentication scheme, while communicating with the ADFS server in the background. Once a user is successfully authenticated, it creates a Thread and HttpContext security contexts, so that the following modules in the .NET request processing pipeline execute in the new security context.

As such, our client can define a Basic auth security using HTTPS transport and it will be able to connect to the service. This is the same mechanism the Content Porter application is using (i.e. Basic auth).

App.config

The .NET Console application I use as test client uses generated service proxy classes. The configuration presented below defines the endpoint to connect to and a simple HTTPS Basic auth transport.

<system.serviceModel>
<bindings>
<basicHttpBinding>
<bindingname="basicHttp">
<securitymode="Transport">
<transportclientCredentialType="Basic"/>
</security>
</binding>
</basicHttpBinding>
</bindings>

<client>
<endpointaddress="https://web85.playground/webservices/ImportExportService201601.svc/basicHttp"
binding="basicHttpBinding"bindingConfiguration="basicHttp"
contract="MyImportExportService.IImportExportService"name="basicHttp"/>
</client>
</system.serviceModel>

Client Code

The client code creates an ImportExportServiceClient which uses the endpoint basicHttp defined in the .config file.

The communication is encrypted using HTTPS and credentials are sent inside the encrypted channel using Basic auth. The HTTP module Basic Authenticator handles the ADFS authentication and tokens and creates the right HttpContext and Thread security principals, so that by the time the request reaches the ImportExport service, it is already authenticated.

publicExample()
{
Console.WriteLine("ImportExport Example");

using (var service = new ImportExportServiceClient("basicHttp"))
{
service.ClientCredentials.UserName.UserName = username;
service.ClientCredentials.UserName.Password = password;

Console.WriteLine("Test Connection: {0}", service.TestConnection());
}
}





Content Porter 8.5 with ADFS

$
0
0
Content Porter uses Basic auth to communicate with the ImportExport service. This means the federated security approach will not work.

Luckily, we have the Basic Authenticator that exposes the ADFS authentication as a basicHttp endpoint. Using this endpoint, the authenticator creates a new security principal and sets it in the current Thread and HttpContext.

As such, Content Porter works again out of the box:


For the Basic Authenticator to intercept requests for the basicHttp endpoint, I defined a regular expression that is matched against the URL of the request:

<add key="BasicAuthPath" value="(?i)/templating/.+\.asmx|/webservices/(core|importexport)service\d{4,6}\.svc/(streamdownload_basichttp|basichttp|script(?!/js))|/wflistener/wflistener\.asmx|/webdav|/monitoring/tridionmonitoringagent.asmx"/>

The expression above matches all requests to:
  • /templating/ folder, and files that end with .asmx extension. These are the services AssemblyTemplateUploadWebService.asmx and CompoundTemplateWebService.asmx
  • CoreService and ImportExportService under /webservices, and files that end with extension .svc, followed by either:
    • streamdownload_BasicHttp -- used for streaming binaries to/from a webservice
    • basicHttp -- exposes Basic authentication to the webservice
    • script, perhaps followed by /js -- exposes the services as web GET method of a REST API
  • /wflistener/wflistener.asmx -- old style Workflow listener webservice
  • /webdav -- exposes WebDAV to Basic auth
  • /Monitoring/TridionMonitoringAgent.asmx -- allows access to the Monitoring Web Service



    TemplateBuilder and Assembly Upload Services with ADFS

    $
    0
    0
    Using either of the templating services (i.e. AssemblyTemplateUploadWebService.asmx and CompoundTemplateWebService.asmx) with ADFS authentication is quite straight forward, if we use the Basic Authenticator. This one exposes a Basic authentication scheme, while communicating with the ADFS server in the background. Once a user is successfully authenticated, it creates a Thread and HttpContext security contexts, so that the following modules in the .NET request processing pipeline execute in the new security context.

    As such, our client can define a Basic auth security using HTTPS transport and it will be able to connect to the service. This is the same mechanism the Content Porter application is using (i.e. Basic auth).

    App.config

    The .NET Console application I use as test client uses generated service proxy classes. The configuration presented below defines the endpoint to connect to and a simple HTTPS Basic auth transport.

    <system.serviceModel>
    <bindings>
    <basicHttpBinding>
    <bindingname="MyBinding">
    <securitymode="Transport">
    <transportclientCredentialType="Basic"/>
    </security>
    </binding>
    </basicHttpBinding>
    </bindings>

    <client>
    <endpointaddress="https://web85.playground/templating/CompoundTemplateWebService.asmx"
    binding="basicHttpBinding"bindingConfiguration="MyBinding"
    contract="MyTemplatingService.CompoundTemplateWebServiceSoap"
    name="CompoundTemplateWebService"/>

    <endpointaddress="https://web85.playground/templating/AssemblyTemplateUploadWebService.asmx"
    binding="basicHttpBinding"bindingConfiguration="MyBinding"
    contract="MyUploadService.AssemblyTemplateUploadWebServiceSoap"
    name="AssemblyTemplateUploadWebServiceSoap"/>
    </client>
    </system.serviceModel>

    Client Code

    The example below connects to the CompoundTemplatingWebService and requests a particular item.

    publicExample1()
    {
    using (var client = new CompoundTemplateWebServiceSoapClient("CompoundTemplateWebService"))
    {
    client.ClientCredentials.UserName.UserName = username;
    client.ClientCredentials.UserName.Password = password;

    var item = client.ReadItem("tcm:0-3-1", EnumOpenMode.OpenModeView, 1);
    Console.WriteLine("Item: " + item.OuterXml);

    var publications = client.GetPublications();
    Console.WriteLine("Publications: " + publications.OuterXml);
    }
    }

    The second example attempts to upload a templating DLL to the AssemblyTemplateUploadWebService:

    publicExample2()
    {
    using (var client = new AssemblyTemplateUploadWebServiceSoapClient("AssemblyTemplateUploadWebServiceSoap"))
    {
    client.ClientCredentials.UserName.UserName = username;
    client.ClientCredentials.UserName.Password = password;

    byte[] dllBytes = File.ReadAllBytes("dll/my-test.dll");

    var result = client.PerformStoreTemplateAssembly(dllBytes, null, "my-test.dll", "tcm:2-9011-2");
    Console.WriteLine("Result: " + result);
    }
    }








    Workflow Listener SDL Web 8.5 with ADFS

    $
    0
    0
    Using the Workflow listener service with ADFS authentication is quite straight forward, if we use the Basic Authenticator. The service WFListener.asmx is not a WCF service, therefore we can't use federation security on it. Instead we need to use Basic authentication and handle the ADFS behind the scene. The Basic Authenticator module exposes a Basic authentication scheme, while communicating with the ADFS server in the background. Once a user is successfully authenticated, it creates a Thread and HttpContext security contexts, so that the following modules in the .NET request processing pipeline execute in the new security context.

    As such, our client can define a Basic auth security using HTTPS transport and it will be able to connect to the service.

    App.config

    The .NET Console application I use as test client uses generated service proxy classes. The configuration presented below defines the endpoint to connect to and a simple HTTPS Basic auth transport.

    <system.serviceModel>
    <bindings>
    <basicHttpBinding>
    <bindingname="WFListenerSoap">
    <securitymode="Transport">
    <transportclientCredentialType="Basic"/>
    </security>
    </binding>
    </basicHttpBinding>
    </bindings>

    <client>
    <endpointaddress="https://web85.playground/WFListener/WFListener.asmx"
    binding="basicHttpBinding"bindingConfiguration="WFListenerSoap"
    contract="WFListenerService.WFListenerSoap"name="WFListenerSoap"/>
    </client>
    </system.serviceModel>

    Client Code

    The client code creates a WFListenerSoapClient which uses the endpoint defined in the .config file.

    The code below is provided as an example only. The real client would be the Microsoft Visio plugin, which talks to the CM server by means of this service.

    publicExample1()
    {
    using (var service = new WFListenerSoapClient("WFListenerSoap"))
    {
    service.ClientCredentials.UserName.UserName = username;
    service.ClientCredentials.UserName.Password = password;

    Console.WriteLine("GetList: {0}", service.GetList());
    }
    }




    JavaScript Client for CoreService

    $
    0
    0
    This blog post shows a way to connect to CoreService directly from a JavaScript client. The CoreService is a WCF web-service, which by default uses SOAP to communicate with its clients. However, it is quite simple to convert it into a REST service that accepts plain POST requests with parameters sent in the request body.

    To keep things simple, I secured the REST endpoint with Basic auth, and thus made use of the Basic Authenticator handler over an HTTPS connection.

    The Server

    The WCF framework allows us to easily expose any web-service as a REST service. For this, I had to modify the file [SDLWebHome]\webservices\Web.config, and add an endpoint behavior under node <system.serviceModel> / <behaviors>. The new behavior enables endpoint to be accessible as REST service:

    <endpointBehaviors>
    <behaviorname="MyJS">
    <enableWebScript/>
    </behavior>
    </endpointBehaviors>

    Next, because we want to use HTTPS, we define a web HTTP binding with security mode Transport, under node <system.serviceModel> / <bindings>:

    <webHttpBinding>
    <bindingname="MyHttps">
    <securitymode="Transport"/>
    </binding>
    </webHttpBinding>

    Next, under our CoreService service definition, we add a new endpoint based on the new MyJS behavior and using the MyHttps binding. Add the following node under <system.serviceModel> / <services>:

    <servicebehaviorConfiguration="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior"
    name="Tridion.ContentManager.ServiceHost.IISHost.CoreService201603">

    <endpointbinding="webHttpBinding"bindingConfiguration="MyHttps"behaviorConfiguration="MyJS"
    bindingNamespace="http://www.sdltridion.com/ContentManager/CoreService/201603"
    contract="Tridion.ContentManager.CoreService.ICoreService201603"address="rest"/>

    This configuration enables a new endpoint rest under the service CoreService201603.svc that accepts HTTPS POST requests to interact with the CoreService. Namely, the endpoint is https://web85.playground/webservices/CoreService201603.svc/rest

    Even more so, the endpoint automatically generates JavaScript proxy classes based on the contract interface defined (i.e. Tridion.ContentManager.CoreService.ICoreService201603). These proxies can be used directly from the server, by loading the following script URL directly in the client web page: https://web85.playground/webservices/CoreService201603.svc/rest/js



    We need to configure one last thing – we must allow cross site scripting. The client JavaScript that will consume our CoreService REST endpoint will most likely make calls to the REST endpoint from a different machine than our Content Manager. This means, we need to configure the remote IPs and the headers that are accepted to make REST calls. For example, a simple way to configure this is to add Access-Control response headers in file [SDLWebHome]\webservices\Web.config, under node <system.webServer>:

    <httpProtocol>
    <customHeaders>
    <addname="Access-Control-Allow-Methods"value="POST,OPTIONS"/>
    <addname="Access-Control-Allow-Origin"value="*"/>
    <addname="Access-Control-Allow-Headers"value="Authorization, Content-Type, X-Requested-With"/>
    </customHeaders>
    </httpProtocol>

    The Client

    The JS proxies generated by the REST endpoint are readily usable, if we use the MicrosoftAjax framework. This is the easiest and quickest way to get the client application going. However, there is one big drawback – the MicrosoftAjax framework doesn’t accept any kind of security configuration when making the AJAX calls to the service (or at least I couldn’t figure it out). Our service requires Basic auth, and it seems there is no way to specify that in the MicrosoftAjax framework. Of course, one can always modify the JS source of the framework, but that would be a hack :)

    In Microsoft Visual Studio (I used 2015), create a new project of type Visual C# / Web / ASP.NET Web Application. This creates all the skeleton code that is needed, including the MicrosoftAjax JS classes.

    The following sample page makes a call to CoreService using the automatically generated proxy classes and MicrosoftAjax framework. Note that this code does not work OOTB in our case, because we are not sending a Basic auth request header:

    <html>
    <head>
    <scriptsrc="/Scripts/jquery-1.10.2.js"></script>
    <scriptsrc="/Scripts/WebForms/MSAjax/MicrosoftAjax.js"></script>
    <scriptsrc="https://web85.playground/webservices/CoreService201603.svc/rest/js"></script>
    </head>
    <body>
    <script>
    var service = new www.sdltridion.com.ContentManager.CoreService._201603.ICoreService();
    service.GetApiVersion(function (result) {
    alert(result);
    });
    </script>
    </body>
    </html>

    The parameter method to GetApiVersion is a callback function that is called if the GetApiVersion invocation is a success. The response from the service is inside variable result.

    Conclusion

    This is a quick and elegant way of exposing a WCF service as a RESTful API without having to rewrite any server code.

    The client however is not very configurable, because we can’t set an authentication method, unless we change the actual framework.

    In a follow-up post, I will show a purely JQuery client that doesn’t depend or use MicrosoftAjax framework, it is much more configurable, and overall smaller in size, but we must write our skeleton proxy classes ourselves.



    JQuery JavaScript Client for CoreService

    $
    0
    0
    In my previous post, I presented a JavaScript CoreService client that was using the out-of-the-box Visual Studio project and a WCF generated JS proxies using the Microsoft AJAX framework.

    In this blog post, I write about a JQuery client for CoreService that is lighter and only depends on JQuery. I took my inspiration from Yoav's blog post Creating a Webservice Proxy with jQuery

    The server part remains untouched. This means I can only focus on the JavaScript client.

    CoreService is secured with Basic or Federated authentication. I am focusing on Basic auth in this post, mainly because it is so simple to use from JS.

    The main entry point in the code is the ServiceProxy class. This is where we define the constructor and where we set the endpoint to use for CoreService and the username, password combination to use.

    The ServiceProxy makes use of the JQuery's $.ajax function, where it performs a POST to the CoreService endpoint, sets the payload to send, Authorization header and the success, failed callback functions.

    ServiceProxy =function (adminUser, adminPassword, endpoint) {
    this._basicAuth ="Basic "+ btoa(adminUser +":"+ adminPassword);
    this._baseURL = endpoint;
    };

    ServiceProxy.prototype =
    {
    _defaultErrorHandler:function (xhr, status, error) {
    alert(xhr.status +""+ xhr.statusText +"\r\n"+ xhr.responseText);
    },

    _doAjax:function (method, data, fnSuccess, fnError, isAsync) {
    if (!data) data = {};
    if (isAsync ==undefined) isAsync =true;
    if (!fnError) fnError =this._defaultErrorHandler;

    $.ajax({
    type:"POST",
    url:this._baseURL + method,
    data: data,
    contentType:"application/json; charset=utf-8",
    headers: { "Authorization":this._basicAuth },
    success: fnSuccess,
    error: fnError,
    async: isAsync
    });
    },

    getCurrentUser:function (success, error) {
    this._doAjax("GetCurrentUser", null, success, error);
    }
    };

    The code above also declares method getCurrentUser, which calls the CoreService GetCurrentUser method without posting any payload.

    The HTML

    The sample code below connects to the CoreService endpoint defined earlier and performs a sample call to retrieve the current user.

    Notice that the JS code only requires the JQuery library and the ServiceProxy we defined earlier.

    Since we are declaring contentType as application/json, JQuery will parse and decode the JSON response from the web-service into a JavaScript object.

    <!DOCTYPE html>
    <html>
    <head>
    <script src="jquery-1.10.2.js"></script>
    <script src="ServiceProxy.js"></script>
    </head>
    <body>
    <script type="text/javascript">
    var proxy =new ServiceProxy("mihai@adfs", "p@assword",
    "https://web85.playground/webservices/CoreService201603.svc/rest/");

    function userSuccess(result) {
    if (!result) {
    alert("no result");
    return;
    }

    if (result.hasOwnProperty("d")) {
    result = result.d;
    }

    alert("Retrieved user: "+ result.Id +" | "+ result.Description);
    }

    $(document).ready(function () {
    proxy.getCurrentUser(userSuccess);
    });
    </script>
    </body>
    </html>




    Create User from JavaScript Client Using CoreService

    $
    0
    0
    This post presents some sample code on how to create a Tridion user from JavaScript client using the CoreService. It is based on the setup presented in previous posts and it only makes use of JQuery library.

    The logic below is also assigning the newly created user to certain Tridion Groups identified either by TCMURI, Name or Description. This means the code will first call the CoreService to retrieve a list of groups, then identify those we need to assign the user to, then create the UserData object with all group memberships set, and then finally send the request to create the user to CoreService.

    The code below is to be inserted into the ServiceProxy class. The entry point is the createUser method, which takes as parameters the user account, user name, and an array of groups to assign the new user. It also takes a success and fail callback functions.

    The code makes use of a number of classes defined below, such as UserData, GroupMembershipData, LinkToGroupData, etc.

    The logic follows roughly the following algorithm:
    • Create user data object and assign user account and user name into it
    • For each group in the array groups
      • find corresponding Tridion group
      • create link to Tridion group using its TCMURI
      • push group membership into the user data
    • send user data to CoreService to create user

    createUser:function (userAccount, userName, groups, success, error) {
    var user =new UserData();
    user.Title = userAccount;
    user.Description = userName;
    user.GroupMemberships = [];

    var i =0;
    while (groupName = groups[i++]) {
    var group =this.findGroup(groupName);
    if (!group) {
    error("Cannot find group "+ groupName);
    return;
    }

    var linkToGroup =new LinkToGroupData();
    linkToGroup.IdRef = group.Id;

    var groupMembership =new GroupMembershipData();
    groupMembership.Group = linkToGroup;

    user.GroupMemberships.push(groupMembership);
    }

    var data = JSON.stringify({ data: user, readBackOptions: {} });
    this._doAjax("Create", data, success, error);
    },

    loadGroups:function () {
    var groups;
    var filter =new GroupsFilterData();
    this._doAjax("GetSystemWideList", JSON.stringify({ filter: filter }), function (result) {
    if (result.hasOwnProperty("d")) {
    result = result.d;
    }
    groups = result;
    }, null, false);

    return groups;
    },

    getGroups:function () {
    if (!this._groups) {
    this._groups =this.loadGroups();
    }
    returnthis._groups;
    },

    findGroup:function (name) {
    if (name.startsWith("tcm:")) {
    return { Id:"<TcmUri xmlns=\"http://www.sdltridion.com/ContentManager/R6\">"+ name +"</TcmUri>" };
    }

    var groups =this.getGroups();
    if (!groups) returnnull;

    var i =0;
    while (group = groups[i++]) {
    if (name == group.Description || name == group.Title) {
    return group;
    }
    }

    returnnull;
    }

    Method findGroup retrieves a list of Groups from Tridion and matches the group given as parameter into the list of groups by TCMURI, description or title.

    Method getGroups retrieves an array of the cached groups it has, otherwise it loads the groups from the server.

    Method loadGroups performs the actual retrieving of a list of groups from Tridion, by performing a synchronous (uhhh... bad) AJAX call to CoreService and sending a request to GetSystemWideList method and passing in a GroupsFilterData object.

    The data classes used above are defined as follows. They are simply identifying the call with a namespace, so that CoreService knows which method to invoke on the web-service:

    GroupsFilterData =function () {
    this.__type ="GroupsFilterData:http://www.sdltridion.com/ContentManager/R6"
    }

    GroupMembershipData =function () {
    this.__type ="GroupMembershipData:http://www.sdltridion.com/ContentManager/R6"
    }

    LinkToGroupData =function () {
    this.__type ="LinkToGroupData:http://www.sdltridion.com/ContentManager/R6"
    }

    UserData =function () {
    this.__type ="UserData:http://www.sdltridion.com/ContentManager/R6"
    }

    The code calling the createUser method above is the following:

    function createSuccess(result) {
    if (!result) {
    alert("no result");
    return;
    }

    if (result.hasOwnProperty("d")) {
    result = result.d;
    }

    alert("Created user with ID: "+ result.Id);
    }

    function createFail(result) {
    if (!result) {
    alert("no result");
    return;
    }

    if (result.hasOwnProperty("d")) {
    result = result.d;
    }

    if (result.hasOwnProperty("responseText")) {
    result = result.responseText;
    }

    alert("Failed: "+ result);
    }

    $(document).ready(function () {
    var userAccount ="ADFS\\mihai";
    var userName ="Mihai Cadariu";
    var groups = ["System Administrator", "tcm:0-3-65568"];

    proxy.createUser(userAccount, userName, groups, createSuccess, createFail);
    });




    Content Delivery Monitoring in AWS with CloudWatch

    $
    0
    0
    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 Worker. The Endpoint uses passive monitoring -- the Monitoring Agent accesses the Endpoint URL using HTTP(S) to read the health status. The Worker uses active monitoring -- it sends heartbeats to the Monitoring Agent reporting health status.

    Configure Content Delivery Heartbeats

    For my Deployer Worker, the monitoring heartbeats are configured in the file deployer-config.xml by adding the following configuration node:

    <MonitoringServiceType="DeployerWorker"Interval="60s"GenerateHeartbeat="true"/>

    At the moment of writing this, the documentation is a big buggy -- I noticed the settings above work, although they yield validation exceptions in the logs.

    Configure the Monitoring Agent

    I'm using the Monitoring Agent to check the health status of the Deployer Endpoint. I'm using the following cd_monitor_conf.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <MonitoringAgentConfigurationVersion="11.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <StartupPeriodStartupValue="60s"/>

    <HeartbeatMonitoringListenerPort="20131"EnableRemoteHeartbeats="true">
    <AutomaticServiceRegistrationRegistrationFile="RegisteredServices.xml"/>
    <Services/>
    </HeartbeatMonitoring>

    <ServiceHealthMonitorBindings>
    <ServiceHealthMonitorBindingName="HttpServiceHealthMonitor"
    Class="com.tridion.monitor.polling.HTTPHealthMonitor"/>
    </ServiceHealthMonitorBindings>

    <ServiceHealthMonitors>
    <HttpServiceHealthMonitorServiceType="DeployerEndpoint"PollInterval="60s"TimeoutInterval="30s">
    <RequestURL="http://localhost:8084/mappings"RequestData=""/>
    <ResponseSuccessPattern="httpupload"/>
    </HttpServiceHealthMonitor>
    </ServiceHealthMonitors>

    <WebServiceListenerPort="20132"/>
    </MonitoringAgentConfiguration>

    Notice that I am not using a Monitoring Agent Web Service, because it is not needed. Instead, I am using the netcat (nc) Unix command to retrieve the statuses from the Monitoring Agent:

    echo"<StatusRequest/>" | nc localhost 20132

    The Monitoring Agent has an in-built simple service that listens on server socket 20132 for incoming connections. If somebody sends the command <StatusRequest/> to this socket, the Monitoring Agent responds with an XML containing statuses for all components it monitors:

    <StatusResponse>
    <ServiceStatus>
    <ServiceType>DeployerEndpoint</ServiceType>
    <ServiceInstance></ServiceInstance>
    <ProcessId>-1</ProcessId>
    <Status>OK</Status>
    <StatusChangeTime>2018-12-22T15:51:07Z</StatusChangeTime>
    <LastReportTime>2018-12-22T15:50:07Z</LastReportTime>
    <MonitoredThreadCount>-1</MonitoredThreadCount>
    </ServiceStatus>
    <ServiceStatus>
    <ServiceType>DeployerWorker</ServiceType>
    <ServiceInstance>dummy</ServiceInstance>
    <ProcessId>7152</ProcessId>
    <Status>OK</Status>
    <StatusChangeTime>2018-12-22T15:50:10Z</StatusChangeTime>
    <LastReportTime>2018-12-22T17:21:14Z</LastReportTime>
    <MonitoredThreadCount>3</MonitoredThreadCount>
    <NonRespondingThreads></NonRespondingThreads>
    </ServiceStatus>
    </StatusResponse>

    The information in this XML is precisely what we want as custom metrics in AWS CloudWatch.

    The Monitoring Agent server socket only listens for connections to 127.0.0.1, so it can't be accessed remotely. This dictates our architecture on how to retrieve this XML response and how to push it into CloudWatch. Enter the lambda...

    AWS Lambda Function

    The function is triggered by a CloudWatch event that fires every so often. In my case, I chose every minute.

    The lambda uses the boto3 API in order to:
    1. Run the nc command remotely on the Deployer instance using SSM API and capture its output
    2. Create custom metrics from the XML output using CloudWatch API
    The code is written in Python 2.7 and looks like this:

    importboto3
    importtime
    fromxml.dom.minidomimport parseString

    statuses = {"OK": 0, "Error": 1, "NotResponding": 2}
    ssmClient = boto3.client('ssm')
    cwClient = boto3.client('cloudwatch')

    deflambda_handler(event, context):
    response = ssmClient.send_command(
    Targets = [{'Key':'tag:Type','Values':['Deployer']}],
    DocumentName ='AWS-RunShellScript',
    TimeoutSeconds =30,
    Parameters = { 'commands': ['echo "<StatusRequest/>" | nc localhost 20132'] }
    )

    commandId = response['Command']['CommandId']
    status = response['Command']['Status']
    while status =='Pending'or status =='InProgress':
    time.sleep(2)
    response = ssmClient.list_commands(CommandId = commandId)
    status = response['Commands'][0]['Status']

    response = ssmClient.list_command_invocations(CommandId = commandId)

    for invocation in response['CommandInvocations']:
    instanceId = invocation['InstanceId']
    instanceName = invocation['InstanceName']
    response = ssmClient.get_command_invocation(CommandId = commandId, InstanceId = instanceId)

    output = response['StandardOutputContent']
    ifnot output:
    continue

    dom = parseString(output)
    statusArray = dom.getElementsByTagName('ServiceStatus')

    for statusEl in statusArray:
    ServiceType = statusEl.getElementsByTagName('ServiceType')[0].firstChild.data
    MetricName ="SDL"+ ServiceType.replace("", "") +"Status"
    Status = statusEl.getElementsByTagName('Status')[0].firstChild.data
    StatusNumber = statuses[Status]

    cwClient.put_metric_data(
    Namespace ='SDL Web',
    MetricData = [{
    'Dimensions': [{
    'Name': 'InstanceName',
    'Value': instanceName
    }],
    'MetricName': MetricName,
    'Value': StatusNumber,
    'Unit': 'None'
    }]
    )

    returnNone


    Brief code explanation:
    • Send nc command to all instances tagged with tag name Type equals Deployer, since I don't feel like keeping track of instance IDs. Currently I have only one instance, but in a production environment the Deployer will be separated into Endpoint and several Worker instances;
    • Wait until command finished execution on all target instances and command status is no longer Pending or InProgress;
    • Read each CommandInvocation within our generic command, so that we are able to retrieve the command output on individual instances;
    • Read the StandardOutputContent from each invocation and parse it into a DOM;
    • For each ServiceStatus node in the XML, translate Status text into a code (0 means OK, 1 = Error and 2 = Not Responding)
    • Push custom metric into CloudWatch using the instance name as dimension (e.g. dev-deployer.mitza.net), ServiceType as metric name, and translated Status as metric value;

    Eventually, when all is working, the following metrics are available in CloudWatch:








    Viewing all 215 articles
    Browse latest View live