//------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------ namespace System.IdentityModel.Tokens { using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IdentityModel.Configuration; using System.IdentityModel.Diagnostics; using System.IdentityModel.Selectors; using System.IO; using System.Runtime; using System.Security.Claims; using System.ServiceModel.Security; using System.Xml; using SessionDictionary = System.IdentityModel.Claims.SessionDictionary; using SysUniqueId = System.Xml.UniqueId; using System.Security.Cryptography.X509Certificates; using System.Runtime.Serialization.Formatters.Binary; /// /// A that processes . /// public class SessionSecurityTokenHandler : SecurityTokenHandler { const string DefaultCookieElementName = "Cookie"; const string DefaultCookieNamespace = "http://schemas.microsoft.com/ws/2006/05/security"; private const string SecureConversationTokenIdentifier = "http://schemas.microsoft.com/ws/2006/05/servicemodel/tokens/SecureConversation"; #pragma warning disable 1591 public static readonly TimeSpan DefaultLifetime = TimeSpan.FromHours(10); public static readonly ReadOnlyCollection DefaultCookieTransforms = (new List(new CookieTransform[] { new DeflateCookieTransform(), new ProtectedDataCookieTransform() }).AsReadOnly()); #pragma warning restore 1591 TimeSpan _tokenLifetime = DefaultLifetime; ReadOnlyCollection _transforms; /// /// Initializes an instance of /// /// /// Properties are used for defaults: /// DefaultCookieTransforms /// DefaultLifetime /// public SessionSecurityTokenHandler() : this(SessionSecurityTokenHandler.DefaultCookieTransforms) { } /// /// Initializes an instance of /// /// The transforms to apply when encoding the cookie. /// /// Properties are used for the remaining defaults: /// DefaultLifetime /// public SessionSecurityTokenHandler(ReadOnlyCollection transforms) : this(transforms, DefaultLifetime) { } /// /// Initializes an instance of /// /// The transforms to apply when encoding the cookie. /// The default for a token. /// Is thrown if 'transforms' is null. /// Is thrown if 'tokenLifetime' is less than or equal to TimeSpan.Zero. public SessionSecurityTokenHandler(ReadOnlyCollection transforms, TimeSpan tokenLifetime) { if (transforms == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("transforms"); } if (tokenLifetime <= TimeSpan.Zero) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID0016))); } _transforms = transforms; _tokenLifetime = tokenLifetime; } /// /// Load custom configuration from Xml /// /// XmlElement to custom configuration. /// The param customConfigElements is null. /// Custom configuration specified was invalid. public override void LoadCustomConfiguration(XmlNodeList customConfigElements) { if (customConfigElements == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("customConfigElements"); } List configNodes = XmlUtil.GetXmlElements(customConfigElements); bool foundValidConfig = false; foreach (XmlElement customConfigElement in configNodes) { if (!StringComparer.Ordinal.Equals(customConfigElement.LocalName, ConfigurationStrings.SessionTokenRequirement)) { continue; } if (foundValidConfig) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID7026, ConfigurationStrings.SessionTokenRequirement))); } _tokenLifetime = DefaultLifetime; foreach (XmlAttribute attribute in customConfigElement.Attributes) { if (StringComparer.OrdinalIgnoreCase.Equals(attribute.LocalName, ConfigurationStrings.Lifetime)) { TimeSpan outTokenLifetime = DefaultLifetime; if (!TimeSpan.TryParse(attribute.Value, out outTokenLifetime)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID7017, attribute.Value))); } if (outTokenLifetime < TimeSpan.Zero) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID7018))); } _tokenLifetime = outTokenLifetime; } else { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID7004, attribute.LocalName, customConfigElement.LocalName))); } } foundValidConfig = true; } } /// /// Gets the name for the cookie element. /// public virtual string CookieElementName { get { return DefaultCookieElementName; } } /// /// Gets the namspace for the cookie element. /// public virtual string CookieNamespace { get { return DefaultCookieNamespace; } } /// /// Applies Transforms to the cookie. /// /// The cookie that will be transformed. /// Controls if the cookie should be encoded (true) or decoded (false) /// Encoded cookie. protected virtual byte[] ApplyTransforms(byte[] cookie, bool outbound) { byte[] transformedCookie = cookie; if (Transforms == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4296))); } if (outbound) { for (int i = 0; i < _transforms.Count; i++) { transformedCookie = _transforms[i].Encode(transformedCookie); } } else { for (int i = _transforms.Count; i > 0; i--) { transformedCookie = _transforms[i - 1].Decode(transformedCookie); } } return transformedCookie; } /// /// Checks the reader if this is a SecurityContextToken. /// /// XmlReader over the incoming SecurityToken. /// 'True' if the reader points to a SecurityContextToken. /// The input argument 'reader' is null. public override bool CanReadToken(XmlReader reader) { if (reader == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("reader"); } return (reader.IsStartElement(WSSecureConversationFeb2005Constants.ElementNames.Name, WSSecureConversationFeb2005Constants.Namespace) || reader.IsStartElement(WSSecureConversation13Constants.ElementNames.Name, WSSecureConversation13Constants.Namespace)); } /// /// Indicates whether this handler supports validation of tokens. /// /// 'True' if the class is capable of SecurityToken validation. public override bool CanValidateToken { get { return true; } } /// /// Gets information on whether this Token Handler can write tokens. /// public override bool CanWriteToken { get { return true; } } /// /// Creates a security token based on a token descriptor. /// /// The token descriptor. /// A security token. /// Thrown if 'tokenDescriptor' is null. public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor) { if (null == tokenDescriptor) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("tokenDescriptor"); } if (this.Configuration == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4272))); } ClaimsPrincipal principal = new ClaimsPrincipal(tokenDescriptor.Subject); if (this.Configuration.SaveBootstrapContext) { SecurityTokenHandlerCollection bootstrapTokenCollection = CreateBootstrapTokenHandlerCollection(); if (!bootstrapTokenCollection.CanWriteToken(tokenDescriptor.Token)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4010, tokenDescriptor.Token.GetType().ToString()))); } (principal.Identities as ReadOnlyCollection)[0].BootstrapContext = new BootstrapContext(tokenDescriptor.Token, bootstrapTokenCollection[tokenDescriptor.Token.GetType()]); } DateTime validFrom = (tokenDescriptor.Lifetime.Created.HasValue) ? (DateTime)tokenDescriptor.Lifetime.Created : DateTime.UtcNow; DateTime validTo = (tokenDescriptor.Lifetime.Expires.HasValue) ? (DateTime)tokenDescriptor.Lifetime.Expires : DateTime.UtcNow + SessionSecurityTokenHandler.DefaultTokenLifetime; return new SessionSecurityToken(principal, null, validFrom, validTo); } /// /// Creates a based on an and a valid time range. /// /// /// Caller defined context string /// Identifier of the endpoint to which the token is scoped. /// Earliest valid time. /// Latest valid time. public virtual SessionSecurityToken CreateSessionSecurityToken( ClaimsPrincipal principal, string context, string endpointId, DateTime validFrom, DateTime validTo) { if (null == principal) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("principal"); } if (this.Configuration == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4272))); } return new SessionSecurityToken(principal, context, endpointId, validFrom, validTo); } /// /// Gets the default token lifetime /// public static TimeSpan DefaultTokenLifetime { get { return DefaultLifetime; } } /// /// Reads the SessionSecurityToken from a stream of bytes. /// /// token. /// SecurityTokenResolver that can be used to resolve the SessionSecurityToken. /// Instance of SessionSecurityToken. public virtual SecurityToken ReadToken(byte[] token, SecurityTokenResolver tokenResolver) { // Our implementation of ReadToken( byte[] ) will always return null. We make the above call not to // break SharePoint. SharePoint has overridden ReadToken(byte[] token) and expect the SessionAuthenticationModule to // call that. So SessionAuthenticationModule will calls this method which does the correct thing. using (XmlReader reader = XmlDictionaryReader.CreateTextReader(token, XmlDictionaryReaderQuotas.Max)) { return this.ReadToken(reader, tokenResolver); } } /// /// Reads the SessionSecurityToken from the given reader. /// /// XmlReader over the SessionSecurityToken. /// An instance of . /// The input argument 'reader' is null. /// The 'reader' is not positioned at a SessionSecurityToken /// or the SessionSecurityToken cannot be read. public override SecurityToken ReadToken(XmlReader reader) { return this.ReadToken(reader, EmptySecurityTokenResolver.Instance); } /// /// Reads the SessionSecurityToken from the given reader. /// /// XmlReader over the SessionSecurityToken. /// SecurityTokenResolver that can used to resolve SessionSecurityToken. /// An instance of . /// The input argument 'reader' is null. /// The 'reader' is not positioned at a SessionSecurityToken /// or the SessionSecurityToken cannot be read. public override SecurityToken ReadToken(XmlReader reader, SecurityTokenResolver tokenResolver) { if (reader == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("reader"); } if (tokenResolver == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("tokenResolver"); } byte[] encodedCookie = null; SysUniqueId contextId = null; SysUniqueId keyGeneration = null; string ns = null; string identifier = null; string instance = null; SecurityToken securityContextToken = null; SessionDictionary dictionary = SessionDictionary.Instance; XmlDictionaryReader dicReader = XmlDictionaryReader.CreateDictionaryReader(reader); if (dicReader.IsStartElement(WSSecureConversationFeb2005Constants.ElementNames.Name, WSSecureConversationFeb2005Constants.Namespace)) { ns = WSSecureConversationFeb2005Constants.Namespace; identifier = WSSecureConversationFeb2005Constants.ElementNames.Identifier; instance = WSSecureConversationFeb2005Constants.ElementNames.Instance; } else if (dicReader.IsStartElement(WSSecureConversation13Constants.ElementNames.Name, WSSecureConversation13Constants.Namespace)) { ns = WSSecureConversation13Constants.Namespace; identifier = WSSecureConversation13Constants.ElementNames.Identifier; instance = WSSecureConversation13Constants.ElementNames.Instance; } else { // // Something is wrong // throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException( SR.GetString(SR.ID4230, WSSecureConversationFeb2005Constants.ElementNames.Name, dicReader.Name))); } string id = dicReader.GetAttribute(WSUtilityConstants.Attributes.IdAttribute, WSUtilityConstants.NamespaceURI); dicReader.ReadFullStartElement(); if (!dicReader.IsStartElement(identifier, ns)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException( SR.GetString(SR.ID4230, WSSecureConversation13Constants.ElementNames.Identifier, dicReader.Name))); } contextId = dicReader.ReadElementContentAsUniqueId(); if (contextId == null || string.IsNullOrEmpty(contextId.ToString())) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.ID4242))); } // // The token can be a renewed token, in which case we need to know the // instance id, which will be the secondary key to the context id for // cache lookups // if (dicReader.IsStartElement(instance, ns)) { keyGeneration = dicReader.ReadElementContentAsUniqueId(); } if (dicReader.IsStartElement(CookieElementName, CookieNamespace)) { // Get the token from the Cache, which is returned as an SCT SecurityToken cachedToken = null; SecurityContextKeyIdentifierClause sctClause = null; if (keyGeneration == null) { sctClause = new SecurityContextKeyIdentifierClause(contextId); } else { sctClause = new SecurityContextKeyIdentifierClause(contextId, keyGeneration); } tokenResolver.TryResolveToken(sctClause, out cachedToken); if (cachedToken != null) { securityContextToken = cachedToken; dicReader.Skip(); } else { // // CookieMode // encodedCookie = dicReader.ReadElementContentAsBase64(); if (encodedCookie == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.ID4237))); } // // appply transforms // byte[] decodedCookie = ApplyTransforms(encodedCookie, false); using (MemoryStream ms = new MemoryStream(decodedCookie)) { BinaryFormatter formatter = new BinaryFormatter(); securityContextToken = formatter.Deserialize(ms) as SecurityToken; } SessionSecurityToken sessionToken = securityContextToken as SessionSecurityToken; if (sessionToken != null && sessionToken.ContextId != contextId) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.ID4229, sessionToken.ContextId, contextId))); } if (sessionToken != null && sessionToken.Id != id) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.ID4227, sessionToken.Id, id))); } } } else { // // SessionMode // // Get the token from the Cache. SecurityToken cachedToken = null; SecurityContextKeyIdentifierClause sctClause = null; if (keyGeneration == null) { sctClause = new SecurityContextKeyIdentifierClause(contextId); } else { sctClause = new SecurityContextKeyIdentifierClause(contextId, keyGeneration); } tokenResolver.TryResolveToken(sctClause, out cachedToken); if (cachedToken != null) { securityContextToken = cachedToken; } } dicReader.ReadEndElement(); if (securityContextToken == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.ID4243))); } return securityContextToken; } /// /// Gets or sets the TokenLifetime. /// public virtual TimeSpan TokenLifetime { get { return _tokenLifetime; } set { if (value <= TimeSpan.Zero) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("value", SR.GetString(SR.ID0016)); } _tokenLifetime = value; } } /// /// Gets the bootstrap token handler collection. /// SecurityTokenHandlerCollection CreateBootstrapTokenHandlerCollection() { SecurityTokenHandlerCollection tokenHandlerCollection = this.ContainingCollection ?? SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(); return tokenHandlerCollection; } /// /// Gets the token type URIs /// public override string[] GetTokenTypeIdentifiers() { return new string[] { SecureConversationTokenIdentifier, WSSecureConversation13Constants.TokenTypeURI, WSSecureConversationFeb2005Constants.TokenTypeURI }; } /// /// Gets the type of token this handler can work with. /// public override Type TokenType { get { return typeof(SessionSecurityToken); } } /// /// Gets the transforms that will be applied to the cookie. /// public ReadOnlyCollection Transforms { get { return _transforms; } } /// /// Sets the transforms that will be applied to cookies. /// /// The objects to use. protected void SetTransforms(IEnumerable transforms) { _transforms = new List(transforms).AsReadOnly(); } /// /// Validates a . /// /// The to validate. /// A of representing the identities contained in the token. /// The parameter 'token' is null. /// The token is not assignable from . public override ReadOnlyCollection ValidateToken(SecurityToken token) { if (token == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("token"); } SessionSecurityToken sessionToken = token as SessionSecurityToken; if (sessionToken == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4292, token.GetType().ToString(), this.GetType().ToString()))); } try { if (DiagnosticUtility.ShouldTrace(TraceEventType.Verbose)) { TraceUtility.TraceEvent( TraceEventType.Verbose, TraceCode.Diagnostics, SR.GetString(SR.TraceValidateToken), new SecurityTraceRecordHelper.TokenTraceRecord(token), null, null); } this.ValidateSession(sessionToken); this.TraceTokenValidationSuccess(token); List identitites = new List(1); identitites.AddRange(sessionToken.ClaimsPrincipal.Identities); return identitites.AsReadOnly(); } catch (Exception e) { if (Fx.IsFatal(e)) { throw; } this.TraceTokenValidationFailure(token, e.Message); throw e; } } /// /// Validates a token and returns its claims. /// /// The to validate. /// Identifier to the endpoint to which the token is scoped. /// A of representing the identities contained in the token. /// The parameter 'token' is null. /// The parameter 'endpointId' is null. /// token.EndpointId != endpointId. public virtual ReadOnlyCollection ValidateToken(SessionSecurityToken token, string endpointId) { if (token == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("token"); } if (endpointId == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("endpointId"); } // We consider SessionTokens with String.Empty as the endpoint Id to be // globally scoped tokens. This in insecure, we are allowing this only // for compatibility with customers who have overriden SessionSecurityTokenHandler. if (!string.IsNullOrEmpty(token.EndpointId)) { if (token.EndpointId != endpointId) { string errorMessage = SR.GetString(SR.ID4291, token); this.TraceTokenValidationFailure(token, errorMessage); throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(errorMessage)); } } return this.ValidateToken(token); } /// /// Checks the valid time of a SecurityToken. /// /// /// The token is invalid if the securityToken.ValidFrom > DateTime.UtcNow OR securityToken.ValidTo < DateTime.UtcNow /// /// The to validate. /// Thrown if 'securityToken' is null. /// Thrown if 'Configuration' is null. /// Thrown if securityToken.ValidFrom > DateTime.UtcNow. /// Thrown if securityToken.ValidTo < DateTime.UtcNow. protected virtual void ValidateSession(SessionSecurityToken securityToken) { if (securityToken == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("securityToken"); } if (this.Configuration == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4274))); } Fx.Assert(this.Configuration != null, SR.GetString(SR.ID8027)); DateTime utcNow = DateTime.UtcNow; // apply clock skew here. DateTime maxTime = DateTimeUtil.Add(utcNow, Configuration.MaxClockSkew); DateTime minTime = DateTimeUtil.Add(utcNow, -Configuration.MaxClockSkew); if (securityToken.ValidFrom > maxTime) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenNotYetValidException(SR.GetString(SR.ID4255, securityToken.ValidTo, securityToken.ValidFrom, utcNow))); } if (securityToken.ValidTo < minTime) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenExpiredException(SR.GetString(SR.ID4255, securityToken.ValidTo, securityToken.ValidFrom, utcNow))); } } /// /// Writes the token into a byte array. /// /// The SessionSecurityToken to write. /// Thrown if 'sessiontoken' is null. /// An encoded byte array. public virtual byte[] WriteToken(SessionSecurityToken sessionToken) { if (sessionToken == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("sessionToken"); } using (MemoryStream ms = new MemoryStream()) { using (XmlWriter writer = XmlWriter.Create(ms)) { WriteToken(writer, sessionToken); writer.Flush(); } return ms.ToArray(); } } /// /// Serializes the given token to the XmlWriter. /// /// XmlWriter to which the token needs to be serialized /// The SecurityToken to be serialized. /// The input argument 'writer' is null. /// The input argument 'token' is either null or not of type /// SessionSecurityToken. public override void WriteToken(XmlWriter writer, SecurityToken token) { if (writer == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("writer"); } if (token == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("token"); } SessionSecurityToken sessionToken = token as SessionSecurityToken; if (sessionToken == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4046, token, TokenType))); } string ns, elementName, contextIdElementName, instance; if (sessionToken.SecureConversationVersion == WSSecureConversationFeb2005Constants.NamespaceUri) { ns = WSSecureConversationFeb2005Constants.Namespace; elementName = WSSecureConversationFeb2005Constants.ElementNames.Name; contextIdElementName = WSSecureConversationFeb2005Constants.ElementNames.Identifier; instance = WSSecureConversationFeb2005Constants.ElementNames.Instance; } else if (sessionToken.SecureConversationVersion == WSSecureConversation13Constants.NamespaceUri) { ns = WSSecureConversation13Constants.Namespace; elementName = WSSecureConversation13Constants.ElementNames.Name; contextIdElementName = WSSecureConversation13Constants.ElementNames.Identifier; instance = WSSecureConversation13Constants.ElementNames.Instance; } else { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.ID4050))); } XmlDictionaryWriter dicWriter; SessionDictionary dictionary = SessionDictionary.Instance; if (writer is XmlDictionaryWriter) { dicWriter = (XmlDictionaryWriter)writer; } else { dicWriter = XmlDictionaryWriter.CreateDictionaryWriter(writer); } dicWriter.WriteStartElement(elementName, ns); if (sessionToken.Id != null) { dicWriter.WriteAttributeString(WSUtilityConstants.Attributes.IdAttribute, WSUtilityConstants.NamespaceURI, sessionToken.Id); } dicWriter.WriteElementString(contextIdElementName, ns, sessionToken.ContextId.ToString()); if (sessionToken.KeyGeneration != null) { dicWriter.WriteStartElement(instance, ns); dicWriter.WriteValue(sessionToken.KeyGeneration); dicWriter.WriteEndElement(); } if (!sessionToken.IsReferenceMode) { dicWriter.WriteStartElement(CookieElementName, CookieNamespace); byte[] cookie; using (MemoryStream ms = new MemoryStream()) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(ms, token); cookie = ms.ToArray(); } cookie = ApplyTransforms(cookie, true); dicWriter.WriteBase64(cookie, 0, cookie.Length); dicWriter.WriteEndElement(); } dicWriter.WriteEndElement(); dicWriter.Flush(); } } }