How to: Create a Custom Token
This topic shows how to create a custom security token using the SecurityToken class, and how to integrate it with a custom security token provider and authenticator.
A security token is essentially an XML element that is used by the Windows Communication Foundation (WCF) security framework to represent claims about a sender inside the SOAP message. WCF security provides various tokens for system-provided authentication modes. Examples include an X.509 certificate security token represented by the X509SecurityToken class or a Username security token represented by the UserNameSecurityToken class.
Sometimes an authentication mode or credential is not supported by the provided types. In that case, it is necessary to create a custom security token to provide an XML representation of the custom credential inside the SOAP message.
The following procedures show how to create a custom security token and how to integrate it with the WCF security infrastructure. This topic creates a credit card token that is used to pass information about the client's credit card to the server.
For more information about custom credentials and security token manager, see How to: Create Custom Client and Service Credentials.
See the System.IdentityModel.Tokens namespace for more classes that represent security tokens.
For more information about credentials, security token manager, and provider and authenticator classes, see Security Architecture.
Procedures
A client application must be provided with a way to specify credit card information for the security infrastructure. This information is made available to the application by a custom client credentials class. The first step is to create a class to represent the credit card information for custom client credentials.
To create a class that represents credit card information inside client credentials
Define a new class that represents the credit card information for the application. The following example names the class
CreditCardInfo
.Add appropriate properties to the class to allow an application set the necessary information required for the custom token. In this example, the class has three properties:
CardNumber
,CardIssuer
, andExpirationDate
.public class CreditCardInfo { string cardNumber; string cardIssuer; DateTime expirationDate; public CreditCardInfo(string cardNumber, string cardIssuer, DateTime expirationDate) { this.cardNumber = cardNumber; this.cardIssuer = cardIssuer; this.expirationDate = expirationDate; } public string CardNumber { get { return this.cardNumber; } } public string CardIssuer { get { return this.cardIssuer; } } public DateTime ExpirationDate { get { return this.expirationDate; } } }
Next, a class that represents the custom security token must be created. This class is used by the security token provider, authenticator, and serializer classes to pass information about the security token to and from the WCF security infrastructure.
To create a custom security token class
Define a new class derived from the SecurityToken class. This example creates a class named
CreditCardToken
.Override the Id property. This property is used to get the local identifier of the security token that is used to point to the security token XML representation from other elements inside the SOAP message. In this example, a token identifier can be either passed to it as a constructor parameter or a new random one is generated every time a security token instance is created.
Implement the SecurityKeys property. This property returns a collection of security keys that the security token instance represents. Such keys can be used by WCF to sign or encrypt parts of the SOAP message. In this example, the credit card security token cannot contain any security keys; therefore, the implementation always returns an empty collection.
Override the ValidFrom and ValidTo properties. These properties are used by WCF to determine the validity of the security token instance. In this example, the credit card security token has only an expiration date, so the
ValidFrom
property returns a DateTime that represents the date and time of the instance creation.class CreditCardToken : SecurityToken { CreditCardInfo cardInfo; DateTime effectiveTime = DateTime.UtcNow; string id; ReadOnlyCollection<SecurityKey> securityKeys; public CreditCardToken(CreditCardInfo cardInfo) : this(cardInfo, Guid.NewGuid().ToString()) { } public CreditCardToken(CreditCardInfo cardInfo, string id) { if (cardInfo == null) { throw new ArgumentNullException("cardInfo"); } if (id == null) { throw new ArgumentNullException("id"); } this.cardInfo = cardInfo; this.id = id; // The credit card token is not capable of any cryptography. this.securityKeys = new ReadOnlyCollection<SecurityKey>(new List<SecurityKey>()); } public CreditCardInfo CardInfo { get { return this.cardInfo; } } public override ReadOnlyCollection<SecurityKey> SecurityKeys { get { return this.securityKeys; } } public override DateTime ValidFrom { get { return this.effectiveTime; } } public override DateTime ValidTo { get { return this.cardInfo.ExpirationDate; } } public override string Id { get { return this.id; } } }
When a new security token type is created, it requires an implementation of the SecurityTokenParameters class. The implementation is used in the security binding element configuration to represent the new token type. The security token parameters class serves as a template that is used to match the actual security token instance to when a message is processed. The template provides additional properties that an application can use to specify criteria that the security token must match to be used or authenticated. The following example does not add any additional properties, so only the security token type is matched when the WCF infrastructure searches for a security token instance to use or to validate.
To create a custom security token parameters class
Define a new class derived from the SecurityTokenParameters class.
Implement the CloneCore method. Copy all internal fields defined in your class, if any. This example does not define any additional fields.
Implement the SupportsClientAuthentication read-only property. This property returns
true
if the security token type represented by this class can be used to authenticate a client to a service. In this example, the credit card security token can be used to authenticate a client to a service.Implement the SupportsServerAuthentication read-only property. This property returns
true
if the security token type represented by this class can be used to authenticate a service to a client. In this example, the credit card security token cannot be used to authenticate a service to a client.Implement the SupportsClientWindowsIdentity read-only property. This property returns
true
if the security token type represented by this class can be mapped to a Windows account. If so, the authentication result is represented by a WindowsIdentity class instance. In this example, the token cannot be mapped to a Windows account.Implement the CreateKeyIdentifierClause method. This method is called by WCF security framework when it requires a reference to the security token instance represented by this security token parameters class. Both the actual security token instance and SecurityTokenReferenceStyle that specifies the type of the reference that is being requested are passed to this method as arguments. In this example, only internal references are supported by the credit card security token. The SecurityToken class has functionality to create internal references; therefore, the implementation does not require additional code.
Implement the InitializeSecurityTokenRequirement method. This method is called by WCF to convert the security token parameters class instance into an instance of the SecurityTokenRequirement class. The result is used by security token providers to create the appropriate security token instance.
public class CreditCardTokenParameters : SecurityTokenParameters { public CreditCardTokenParameters() { } protected CreditCardTokenParameters(CreditCardTokenParameters other) : base(other) { } protected override SecurityTokenParameters CloneCore() { return new CreditCardTokenParameters(this); } protected override void InitializeSecurityTokenRequirement(SecurityTokenRequirement requirement) { requirement.TokenType = Constants.CreditCardTokenType; return; } // A credit card token has no cryptography, no windows identity, and supports only client authentication. protected override bool HasAsymmetricKey { get { return false; } } protected override bool SupportsClientAuthentication { get { return true; } } protected override bool SupportsClientWindowsIdentity { get { return false; } } protected override bool SupportsServerAuthentication { get { return false; } } protected override SecurityKeyIdentifierClause CreateKeyIdentifierClause(SecurityToken token, SecurityTokenReferenceStyle referenceStyle) { if (referenceStyle == SecurityTokenReferenceStyle.Internal) { return token.CreateKeyIdentifierClause<LocalIdKeyIdentifierClause>(); } else { throw new NotSupportedException("External references are not supported for credit card tokens"); } } }
Security tokens are transmitted inside SOAP messages, which requires a translation mechanism between the in-memory security token representation and the on-the-wire representation. WCF uses a security token serializer to accomplish this task. Every custom token must be accompanied by a custom security token serializer that can serialize and deserialize the custom security token from the SOAP message.
Note: |
---|
Derived keys are enabled by default. If you create a custom security token and use it as the primary token, WCF derives a key from it. While doing so, it calls the custom security token serializer to write the SecurityKeyIdentifierClause for the custom security token while serializing the DerivedKeyToken to the wire. On the receiving end, when deserializing the token off the wire, the DerivedKeyToken serializer expects a SecurityTokenReference element as the top-level child under itself. If the custom security token serializer did not add a SecurityTokenReference element while serializing its clause type, an exception is thrown. |
To create a custom security token serializer
Define a new class derived from the WSSecurityTokenSerializer class.
Override the CanReadTokenCore method, which relies on an XmlReader to read the XML stream. The method returns
true
if the serializer implementation can deserialize the security token based given its current element. In this example, this method checks whether the XML reader's current XML element has the correct element name and namespace. If it does not, it calls the base class implementation of this method to handle the XML element.Override the ReadTokenCore method. This method reads the XML content of the security token and constructs the appropriate in-memory representation for it. If it does not recognize the XML element on which the passed-in XML reader is standing, it calls the base class implementation to process the system-provided token types.
Override the CanWriteTokenCore method. This method returns
true
if it can convert the in-memory token representation (passed in as an argument) to the XML representation. If it cannot convert, it calls the base class implementation.Override the WriteTokenCore method. This method converts an in-memory security token representation into an XML representation. If the method cannot convert, it calls the base class implementation.
public class CreditCardSecurityTokenSerializer : WSSecurityTokenSerializer { public CreditCardSecurityTokenSerializer(SecurityTokenVersion version) : base() { } protected override bool CanReadTokenCore(XmlReader reader) { XmlDictionaryReader localReader = XmlDictionaryReader.CreateDictionaryReader(reader); if (reader == null) { throw new ArgumentNullException("reader"); } if (reader.IsStartElement(Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace)) { return true; } return base.CanReadTokenCore(reader); } protected override SecurityToken ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver) { if (reader == null) { throw new ArgumentNullException("reader"); } if (reader.IsStartElement(Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace)) { string id = reader.GetAttribute(Constants.Id, Constants.WsUtilityNamespace); reader.ReadStartElement(); // Read the credit card number. string creditCardNumber = reader.ReadElementString(Constants.CreditCardNumberElementName, Constants.CreditCardTokenNamespace); // Read the expiration date. string expirationTimeString = reader.ReadElementString(Constants.CreditCardExpirationElementName, Constants.CreditCardTokenNamespace); DateTime expirationTime = XmlConvert.ToDateTime(expirationTimeString, XmlDateTimeSerializationMode.Utc); // Read the issuer of the credit card. string creditCardIssuer = reader.ReadElementString(Constants.CreditCardIssuerElementName, Constants.CreditCardTokenNamespace); reader.ReadEndElement(); CreditCardInfo cardInfo = new CreditCardInfo(creditCardNumber, creditCardIssuer, expirationTime); return new CreditCardToken(cardInfo, id); } else { return WSSecurityTokenSerializer.DefaultInstance.ReadToken(reader, tokenResolver); } } protected override bool CanWriteTokenCore(SecurityToken token) { if (token is CreditCardToken) { return true; } else { return base.CanWriteTokenCore(token); } } protected override void WriteTokenCore(XmlWriter writer, SecurityToken token) { if (writer == null) { throw new ArgumentNullException("writer"); } if (token == null) { throw new ArgumentNullException("token"); } CreditCardToken c = token as CreditCardToken; if (c != null) { writer.WriteStartElement(Constants.CreditCardTokenPrefix, Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace); writer.WriteAttributeString(Constants.WsUtilityPrefix, Constants.Id, Constants.WsUtilityNamespace, token.Id); writer.WriteElementString(Constants.CreditCardNumberElementName, Constants.CreditCardTokenNamespace, c.CardInfo.CardNumber); writer.WriteElementString(Constants.CreditCardExpirationElementName, Constants.CreditCardTokenNamespace, XmlConvert.ToString(c.CardInfo.ExpirationDate, XmlDateTimeSerializationMode.Utc)); writer.WriteElementString(Constants.CreditCardIssuerElementName, Constants.CreditCardTokenNamespace, c.CardInfo.CardIssuer); writer.WriteEndElement(); writer.Flush(); } else { base.WriteTokenCore(writer, token); } } }
After completing the four previous procedures, integrate the custom security token with the security token provider, authenticator, manager, and client and service credentials.
To integrate the custom security token with a security token provider
The security token provider creates, modifies (if necessary), and returns an instance of the token. To create a custom provider for the custom security token, create a class that inherits from the SecurityTokenProvider class. The following example overrides the GetTokenCore method to return an instance of the
CreditCardToken
. For more information about custom security token providers, see How to: Create a Custom Security Token Provider.class CreditCardTokenProvider : SecurityTokenProvider { CreditCardInfo creditCardInfo; public CreditCardTokenProvider(CreditCardInfo creditCardInfo) : base() { if (creditCardInfo == null) { throw new ArgumentNullException("creditCardInfo"); } this.creditCardInfo = creditCardInfo; } protected override SecurityToken GetTokenCore(TimeSpan timeout) { SecurityToken result = new CreditCardToken(this.creditCardInfo); return result; } }
To integrate the custom security token with a security token authenticator
The security token authenticator validates the content of the security token when it is extracted from the message. To create a custom authenticator for the custom security token, create a class that inherits from the SecurityTokenAuthenticator class. The following example overrides the ValidateTokenCore method. For more information about custom security token authenticators, see How to: Create a Custom Security Token Authenticator.
class CreditCardTokenAuthenticator : SecurityTokenAuthenticator { string creditCardsFile; public CreditCardTokenAuthenticator(string creditCardsFile) { this.creditCardsFile = creditCardsFile; } protected override bool CanValidateTokenCore(SecurityToken token) { return (token is CreditCardToken); } protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token) { CreditCardToken creditCardToken = token as CreditCardToken; if (creditCardToken.CardInfo.ExpirationDate < DateTime.UtcNow) { throw new SecurityTokenValidationException("The credit card has expired"); } if (!IsCardNumberAndExpirationValid(creditCardToken.CardInfo)) { throw new SecurityTokenValidationException("Unknown or invalid credit card"); } // The credit card token has only 1 claim: the card number. The issuer for the claim is the // credit card issuer. DefaultClaimSet cardIssuerClaimSet = new DefaultClaimSet(new Claim(ClaimTypes.Name, creditCardToken.CardInfo.CardIssuer, Rights.PossessProperty)); DefaultClaimSet cardClaimSet = new DefaultClaimSet(cardIssuerClaimSet, new Claim(Constants.CreditCardNumberClaim, creditCardToken.CardInfo.CardNumber, Rights.PossessProperty)); List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>(1); policies.Add(new CreditCardTokenAuthorizationPolicy(cardClaimSet)); return policies.AsReadOnly(); } // This helper method checks whether a given credit card entry is present in the user database. private bool IsCardNumberAndExpirationValid(CreditCardInfo cardInfo) { try { using (StreamReader myStreamReader = new StreamReader(this.creditCardsFile)) { string line = ""; while ((line = myStreamReader.ReadLine()) != null) { string[] splitEntry = line.Split('#'); if (splitEntry[0] == cardInfo.CardNumber) { string expirationDateString = splitEntry[1].Trim(); DateTime expirationDateOnFile = DateTime.Parse(expirationDateString, System.Globalization.DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.AdjustToUniversal); if (cardInfo.ExpirationDate == expirationDateOnFile) { string issuer = splitEntry[2]; return issuer.Equals(cardInfo.CardIssuer, StringComparison.InvariantCultureIgnoreCase); } else { return false; } } } return false; } } catch (Exception e) { throw new Exception("BookStoreService: Error while retrieving credit card information from User DB " + e.ToString()); } } }
public class CreditCardTokenAuthorizationPolicy : IAuthorizationPolicy { string id; ClaimSet issuer; IEnumerable<ClaimSet> issuedClaimSets; public CreditCardTokenAuthorizationPolicy(ClaimSet issuedClaims) { if (issuedClaims == null) throw new ArgumentNullException("issuedClaims"); this.issuer = issuedClaims.Issuer; this.issuedClaimSets = new ClaimSet[] { issuedClaims }; this.id = Guid.NewGuid().ToString(); } public ClaimSet Issuer { get { return this.issuer; } } public string Id { get { return this.id; } } public bool Evaluate(EvaluationContext context, ref object state) { foreach (ClaimSet issuance in this.issuedClaimSets) { context.AddClaimSet(this, issuance); } return true; } }
To integrate the custom security token with a security token manager
The security token manager creates the appropriate token provider, security authenticator, and token serializer instances. To create a custom token manager, create a class that inherits from the ClientCredentialsSecurityTokenManager class. The primary methods of the class use a SecurityTokenRequirement to create the appropriate provider and client or service credentials. For more information about custom security token managers, see How to: Create Custom Client and Service Credentials.
public class CreditCardClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager { CreditCardClientCredentials creditCardClientCredentials; public CreditCardClientCredentialsSecurityTokenManager(CreditCardClientCredentials creditCardClientCredentials) : base(creditCardClientCredentials) { this.creditCardClientCredentials = creditCardClientCredentials; } public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement) { if (tokenRequirement.TokenType == Constants.CreditCardTokenType) { // Handle this token for Custom. return new CreditCardTokenProvider(this.creditCardClientCredentials.CreditCardInfo); } else if (tokenRequirement is InitiatorServiceModelSecurityTokenRequirement) { // Return server certificate. if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate) { return new X509SecurityTokenProvider(creditCardClientCredentials.ServiceCertificate.DefaultCertificate); } } return base.CreateSecurityTokenProvider(tokenRequirement); } public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version) { return new CreditCardSecurityTokenSerializer(version); } }
public class CreditCardServiceCredentialsSecurityTokenManager : ServiceCredentialsSecurityTokenManager { CreditCardServiceCredentials creditCardServiceCredentials; public CreditCardServiceCredentialsSecurityTokenManager(CreditCardServiceCredentials creditCardServiceCredentials) : base(creditCardServiceCredentials) { this.creditCardServiceCredentials = creditCardServiceCredentials; } public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver) { if (tokenRequirement.TokenType == Constants.CreditCardTokenType) { outOfBandTokenResolver = null; return new CreditCardTokenAuthenticator(creditCardServiceCredentials.CreditCardDataFile); } return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver); } public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version) { return new CreditCardSecurityTokenSerializer(version); } }
To integrate the custom security token with custom client and service credentials
The custom client and service credentials must be added to provide an API for the application to allow specifying custom token information that is used by the custom security token infrastructure created previously to provide and authenticate the custom security token content. The following samples show how this can be done. For more information about custom client and service credentials, see How to: Create Custom Client and Service Credentials.
public class CreditCardClientCredentials : ClientCredentials { CreditCardInfo creditCardInfo; public CreditCardClientCredentials(CreditCardInfo creditCardInfo) : base() { if (creditCardInfo == null) { throw new ArgumentNullException("creditCardInfo"); } this.creditCardInfo = creditCardInfo; } public CreditCardInfo CreditCardInfo { get { return this.creditCardInfo; } } protected override ClientCredentials CloneCore() { return new CreditCardClientCredentials(this.creditCardInfo); } public override SecurityTokenManager CreateSecurityTokenManager() { return new CreditCardClientCredentialsSecurityTokenManager(this); } }
public class CreditCardServiceCredentials : ServiceCredentials { string creditCardFile; public CreditCardServiceCredentials(string creditCardFile) : base() { if (creditCardFile == null) { throw new ArgumentNullException("creditCardFile"); } this.creditCardFile = creditCardFile; } public string CreditCardDataFile { get { return this.creditCardFile; } } protected override ServiceCredentials CloneCore() { return new CreditCardServiceCredentials(this.creditCardFile); } public override SecurityTokenManager CreateSecurityTokenManager() { return new CreditCardServiceCredentialsSecurityTokenManager(this); } }
The custom security token parameters class created previously is used to tell the WCF security framework that a custom security token must be used when communicating with a service. The following procedure shows how this can be done.
To integrate the custom security token with the binding
The custom security token parameters class must be specified in one of the token parameters collections that are exposed on the SecurityBindingElement class. The following example uses the collection returned by SignedEncrypted. The code adds the credit card custom token to every message sent from the client to the service with its content automatically signed and encrypted.
public static class BindingHelper { public static Binding CreateCreditCardBinding() { HttpTransportBindingElement httpTransport = new HttpTransportBindingElement(); // The message security binding element is configured to require a credit card // token that is encrypted with the service's certificate. SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement(); messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new CreditCardTokenParameters()); X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters(); x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never; messageSecurity.ProtectionTokenParameters = x509ProtectionParameters; return new CustomBinding(messageSecurity, httpTransport); } }
See Also
Tasks
How to: Create a Custom Security Token Provider
Reference
SecurityToken
SecurityTokenParameters
WSSecurityTokenSerializer
SecurityTokenProvider
SecurityTokenAuthenticator
IAuthorizationPolicy
SecurityTokenRequirement
SecurityTokenManager
ClientCredentials
ServiceCredentials
SecurityBindingElement
Concepts
How to: Create Custom Client and Service Credentials
How to: Create a Custom Security Token Authenticator
Security Architecture