Please note that the code provided is _not_ production
ready.
The two scenarios we are looking at:
1.
Identity federation: enable claims based
authentication on wcf services by dynamically configuring WCF and WIF. Ideal
for active clients like thick clients and background services.
2.
Identity delegation: allows a user identity to
be reused by a trusted service account. Ideal for passive clients like web
browsers.
Because ACS does not support ActAs we end up with a slightly
more complex example that I initially imagined. Here is how this sample works:
The end user authenticates with Azure ACS (in the sample ACS
is configured to authenticate with Facebook, google, Microsoft or Yahoo!) while
the web server authenticates the service account with ADFS only. Both the user
and the service account must have valid users that is authenticated.
Time for code (enough with the abstract stuff).
First we need to add a new binding builder for the secure
binding.
internal class SecureBindingBuilder : IBindingBuilder
{
private bool IsClient;
public Binding
CreateBinding(string serviceName)
{
System.Net.ServicePointManager.ServerCertificateValidationCallback =
((sender,
certificate, chain, sslPolicyErrors) => true);
var secureBinding =
new
WS2007FederationHttpBinding(WSFederationHttpSecurityMode.TransportWithMessageCredential)
{
Name
= "secure",
TransactionFlow =
false,
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
MaxBufferPoolSize = int.Parse(GetWsValue(serviceName,
"MaxBufferPoolSize")),
MaxReceivedMessageSize = int.Parse(GetWsValue(serviceName,
"MaxReceivedSize")),
MessageEncoding =
(WSMessageEncoding)Enum.Parse(typeof(WSMessageEncoding),
GetWsValue(serviceName, "MessageFormat")),
TextEncoding = Encoding.GetEncoding(GetWsValue(serviceName,
"TextEncoding")),
ReaderQuotas = XmlDictionaryReaderQuotas.Max,
BypassProxyOnLocal = true,
UseDefaultWebProxy = false,
};
SetSecuritySettings(serviceName, secureBinding);
return
secureBinding;
}
private void
SetSecuritySettings(string serviceName, WSFederationHttpBinding
secureBinding)
{
secureBinding.Security.Message.AlgorithmSuite =
SecurityAlgorithmSuite.Basic256;
secureBinding.Security.Message.NegotiateServiceCredential = false;
secureBinding.Security.Message.EstablishSecurityContext = false;
secureBinding.Security.Message.IssuedKeyType =
SecurityKeyType.BearerKey;
secureBinding.Security.Message.IssuedTokenType = null;
EndpointIdentity
identity = EndpointIdentity.CreateDnsIdentity((new
Uri(AddressHelper.GetIssuerName(serviceName)).DnsSafeHost));
secureBinding.Security.Message.IssuerAddress
= CreateIssuerAddress(serviceName,identity);
if (IsClient)
secureBinding.Security.Message.IssuerBinding =
CreateIssuerBinding(serviceName);
else
secureBinding.Security.Message.IssuerMetadataAddress = new
EndpointAddress(new
Uri(AddressHelper.GetStsMetadataAddress(serviceName)),identity);
}
private EndpointAddress CreateIssuerAddress(string
serviceName,EndpointIdentity identity)
{
if(IsClient)
return new
EndpointAddress(new Uri(AddressHelper.GetStsActAsAddress(serviceName)));
return new
EndpointAddress(new Uri(AddressHelper.GetIssuerName(serviceName)));
}
private static Binding CreateIssuerBinding(string
serviceName)
{
return
BindingFactory.GetBindingBuilder("stsbinding").CreateBinding(serviceName);
}
private static string
GetWsValue(string serviceName, string valueName)
{
var inheritFrom =
GetSecureValue(serviceName, "InheritFrom");
return
ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName,
inheritFrom, valueName));
}
private static string
GetSecureValue(string serviceName, string valueName)
{
return
ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName,
"secure", valueName));
}
public void
SetClientMode(bool isClient)
{
IsClient = isClient;
}
}
|
Note that we use the WS2007FederationHttpBinding which has
most of the binding elements for WIF added. The first line in CreateBinding is
added only to allow connections over SSL with self signed certificates, this
should be removed in production code or made optional though the configuration
framework.
For WIF to hook properly into the WCF framework we need to
do some configuration on the service host. Create a helper method in
HostFactory called Configure to make this independent of the service
implementation.
public static void Configure(ServiceConfiguration config)
{
var serviceName =
GetServiceName(config.Description.ServiceType);
config.IdentityConfiguration = new IdentityConfiguration()
{
TrustedStoreLocation = new StoreLocation(),
AudienceRestriction
= { AudienceMode = AudienceUriMode.Always },
SaveBootstrapContext = true,
IssuerTokenResolver = new IssuerTokenResolver(),
CertificateValidationMode = X509CertificateValidationMode.None,
};
config.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new
Uri(GetSecureValue(serviceName, "Audience")));
config.IdentityConfiguration.IssuerNameRegistry =
CreateIssuerNameRegistry(serviceName);
config.UseIdentityConfiguration = true;
}
private static ConfigurationBasedIssuerNameRegistry
CreateIssuerNameRegistry(string serviceName)
{
var registry = new
ConfigurationBasedIssuerNameRegistry();
registry.AddTrustedIssuer(GetSecureValue(serviceName,
"Thumbprint"), GetSecureValue(serviceName,
"IssuerName"));
return registry;
}
|
We need to run this code from the service implementation (to
me it seems that this is convention based) by adding the following code in
DemoService.svc.cs
{
HostFactory.Configure(config);
}
|
When this is done we need some way to get security tokens
from ADFS. By isolating the needed code in a TokenManager class, we reduce the
number of changes needed in the ServiceProxyContainer class.
public class TokenManager
{
private readonly string
ServiceName;
private readonly string
UserName;
private readonly string
Password;
private readonly
SecurityToken BootstrapToken;
public
TokenManager(string serviceName, string username,string password)
{
UserName =
username;
Password =
password;
ServiceName =
serviceName;
}
public
TokenManager(string serviceName,SecurityToken bootstrapToken, string
username, string password)
{
UserName =
username;
Password =
password;
ServiceName =
serviceName;
BootstrapToken =
bootstrapToken;
}
public SecurityToken
GetToken()
{
var channel =
CreateChannel();
var rst =
CreateIssueNewRequest();
return
GetSecurityToken(channel, rst);
}
private RequestSecurityToken
CreateIssueNewRequest()
{
var rst = new
RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
AppliesTo =
new EndpointReference(
ConfigurationReader.Reader.GetValue(
ExpressionBuilder.GetEndpointConfigValue(
ServiceName,
"secure", "Address")))
};
return rst;
}
private static
SecurityToken GetSecurityToken(WSTrustChannel channel, RequestSecurityToken
rst)
{
RequestSecurityTokenResponse rstr =
null;
var token =
channel.Issue(rst, out rstr);
return token;
}
private WSTrustChannel
CreateChannel()
{
var stsBinding =
CreateStsBinding();
var trustChannelFactory = new
WSTrustChannelFactory(stsBinding, AddressHelper.GetStsAddress(ServiceName))
{
TrustVersion =
TrustVersion
.WSTrust13
};
trustChannelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode
=
X509CertificateValidationMode.None;
trustChannelFactory.Credentials.UserName.UserName = UserName;
trustChannelFactory.Credentials.UserName.Password = Password;
trustChannelFactory.Endpoint.Behaviors.Add(new
MustUnderstandBehavior(false));
var channel =
(WSTrustChannel)trustChannelFactory.CreateChannel();
return channel;
}
private Binding
CreateStsBinding()
{
return
BindingFactory.GetBindingBuilder("stsbinding").CreateBinding(ServiceName);
}
public SecurityToken
GetTokenOnBehalfOf()
{
var channel =
CreateChannel();
var rst =
CreateDelegateTokenRequest();
return
GetSecurityToken(channel, rst);
}
private
RequestSecurityToken CreateDelegateTokenRequest()
{
var rst = new
RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
AppliesTo =
new EndpointReference(
ConfigurationReader.Reader.GetValue(
ExpressionBuilder.GetEndpointConfigValue(
ServiceName, "secure", "Address"))),
ActAs = new SecurityTokenElement(BootstrapToken)
};
return rst;
}
}
|
As you can see, we reuse the binding builder for the STS binding both in the client and the service. This set up the communication with, in this case, ADFS.
Create a new channel factory based on this binding, and set
the client credentials. Regardless whether we are using delegate tokens or
direct logon the channel will be the same. In the sample code we do not perform
certificate validation. After the channel is configured and created, we create
a request and call the Issue method on the channel. The token we receive will
be used when we create the service channel in the container.
As a last step we need to modify the ServiceProxyContainer:
Add to methods to get security tokens:
private SecurityToken GetToken()
{
var manager = new
TokenManager(ServiceName, UserName, Password);
return
manager.GetToken();
}
private SecurityToken
GetTokenOnBehalfOf()
{
var manager = new
TokenManager(ServiceName,GetTokenFromBootstrap(), UserName, Password);
return
manager.GetTokenOnBehalfOf();
}
|
And create the channel that uses the issued security token:
public T GetClient()
{
if (Client == null)
{
if
(BootstrapContext != null) Client =
ClientFactory.CreateChannelWithIssuedToken(GetTokenOnBehalfOf());
else if
(UseSecureChannel) Client = ClientFactory.CreateChannelWithIssuedToken(GetToken());
else Client =
ClientFactory.CreateChannel(new EndpointAddress(Url));
}
return Client;
}
|
Handling the tokens in this way makes it a lot easier to do
debugging and to inspect the tokens before they are sent to the service.
This is basically it.
I have added a simple MVC app to show delegation in action.
While the console application is mainly the same.
Some help with setting up ADFS to forward name identifier in
the issued token.
http://social.msdn.microsoft.com/Forums/vstudio/en-US/2fbd4acf-358e-4bf3-abf6-fc2c9daa20ca/incoming-name-id-format-for-incoming-claim-type-name-id-in-adfs-v20
I hope you enjoyed these posts, I might add some samples on
MSMQ and Net.TCP if you are interested. However, it should be easy to add this
using the sample code provided as a template.