Reliable Messaging and SecurityToken validation
One of the things that have come up many times is how the service could stop a client from retrying a request for a valid security validation error while Reliable Messaging is enabled. If you are not familiar with the situation the essence of the problem is this,
Binding on the Service has Reliable Messaging (RM) enabled. You can do this using WsHttpBinding and setting the ReliableSession.Enabled property to true. What this would mean is that the client will re-try the request when the service responds with any random failure, after a session has been established. By random failure I mean failures that does not close the RM session while sending back the response. A fault sent back with proper RM headers to close the message would not result in a retry of the failed request. Unfortunately all SecurityToken validation and SecurityHeader validation exceptions are treated random exceptions as the response does not contain any required header or is the response secured.
One of the most common cases when this happens is when RM is enabled and a Username/Password validation fails. WCF provides extensibility points to plug in your Custom Username/Password validator, but any exception from the validator does not close the RM session and hence the client keep retrying the request until it finally times out. The post discusses a work around to close the RM session when such failures occur.
Write a Custom Username/Password Authenticator and plug this into the service using a Custom ServiceCredentials. The Custom Authenticator should add a specific failure claim to the AuthorizationContext. A sample code for the Custom Username/Password Authenticator is shown below.
class CustomUsernamePasswordAuthenticator : UserNameSecurityTokenAuthenticator
{
protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateUserNamePasswordCore(string userName, string password)
{
Claim claim = null;
if (String.CompareOrdinal(userName, password) == 0)
{
claim = Claim.CreateNameClaim(userName);
}
else
{
claim = new Claim("https://contoso.com/InvalidUsernameClaim", true, Rights.PossessProperty);
}
List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
List<ClaimSet> claimsets = new List<ClaimSet>();
claimsets.Add(new DefaultClaimSet(claim));
policies.Add(new ClaimFactoryPolicy(claimsets.AsReadOnly()));
return policies.AsReadOnly();
}
}
As you can see the above code is adding a specific claim of type https://contoso.com/InvalidUsernameClaim to the AuthorizationContext. For more information on how to plug custom authenticators in WCF you can take a look at https://msdn2.microsoft.com/en-us/library/ms730079.aspx.
The next we would do is to write a Custom Service Authorization Manager (SAM). The SAM gets called when the request has finally passed through all the binding elements so the RM header on the Request has been consumed. When an Access Denied result is returned by the SAM the failure response returned will be returned as Access Denied fault with the RM header enabled in the response that closes the RM session. Our Custom SAML will look for the specific Claim of type https://contoso.com/InvalidUsernameClaim to check whether to Authorize the user or not.
class CustomServiceAuthorizationManager : ServiceAuthorizationManager
{
public override bool CheckAccess(OperationContext operationContext)
{
ReadOnlyCollection<ClaimSet> claimsets = operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets;
foreach (ClaimSet claimSet in claimsets)
{
if (claimSet.ContainsClaim(new Claim("https://contoso.com/InvalidUsernameClaim", true, Rights.PossessProperty)))
{
return false;
}
}
return true;
}
}
Custom SAM can be plugged into the ServiceCredentials as shown below,
service.Authorization.ServiceAuthorizationManager = new CustomServiceAuthorizationManager();
The fault returned by the Custom SAM will stop the client from retrying when a token validation failure happens on the Service end.
Comments
- Anonymous
October 04, 2007
Wouldn’t you like to show your users a custom error message instead of this generic one? Now you can