//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.Messages { using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net.Security; using System.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.Messaging.Reflection; using DotNetOpenAuth.OpenId.ChannelElements; using Validation; /// /// An indirect message from a Provider to a Relying Party where at least part of the /// payload is signed so the Relying Party can verify it has not been tampered with. /// [DebuggerDisplay("OpenID {Version} {Mode} (no id assertion)")] [Serializable] [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1630:DocumentationTextMustContainWhitespace", Justification = "The samples are string literals.")] [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1631:DocumentationMustMeetCharacterPercentage", Justification = "The samples are string literals.")] internal class IndirectSignedResponse : IndirectResponseBase, ITamperResistantOpenIdMessage { /// /// The allowed date/time formats for the response_nonce parameter. /// /// /// This array of formats is not yet a complete list. /// private static readonly string[] PermissibleDateTimeFormats = { "yyyy-MM-ddTHH:mm:ssZ" }; /// /// Backing field for the property. /// /// /// The field initializer being DateTime.UtcNow allows for OpenID 1.x messages /// to pass through the StandardExpirationBindingElement. /// private DateTime creationDateUtc = DateTime.UtcNow; /// /// Backing store for the property. /// private IDictionary returnToParameters; /// /// Initializes a new instance of the class. /// /// /// The authentication request that caused this assertion to be generated. /// internal IndirectSignedResponse(SignedResponseRequest request) : base(request, Protocol.Lookup(GetVersion(request)).Args.Mode.id_res) { Requires.NotNull(request, "request"); this.ReturnTo = request.ReturnTo; this.ProviderEndpoint = request.Recipient.StripQueryArgumentsWithPrefix(Protocol.openid.Prefix); ((ITamperResistantOpenIdMessage)this).AssociationHandle = request.AssociationHandle; } /// /// Initializes a new instance of the class /// in order to perform signature verification at the Provider. /// /// The previously signed message. /// The channel. This is used only within the constructor and is not stored in a field. internal IndirectSignedResponse(CheckAuthenticationRequest previouslySignedMessage, Channel channel) : base(GetVersion(previouslySignedMessage), previouslySignedMessage.ReturnTo, Protocol.Lookup(GetVersion(previouslySignedMessage)).Args.Mode.id_res) { Requires.NotNull(channel, "channel"); // Copy all message parts from the check_authentication message into this one, // except for the openid.mode parameter. MessageDictionary checkPayload = channel.MessageDescriptions.GetAccessor(previouslySignedMessage); MessageDictionary thisPayload = channel.MessageDescriptions.GetAccessor(this); foreach (var pair in checkPayload) { if (!string.Equals(pair.Key, this.Protocol.openid.mode)) { thisPayload[pair.Key] = pair.Value; } } } /// /// Initializes a new instance of the class /// for unsolicited assertions. /// /// The OpenID version to use. /// The return_to URL of the Relying Party. /// This value will commonly be from , /// but for unsolicited assertions may come from the Provider performing RP discovery /// to find the appropriate return_to URL to use. internal IndirectSignedResponse(Version version, Uri relyingPartyReturnTo) : base(version, relyingPartyReturnTo, Protocol.Lookup(version).Args.Mode.id_res) { this.ReturnTo = relyingPartyReturnTo; } /// /// Gets the level of protection this message requires. /// /// /// for OpenID 2.0 messages. /// for OpenID 1.x messages. /// /// /// Although the required protection is reduced for OpenID 1.x, /// this library will provide Relying Party hosts with all protections /// by adding its own specially-crafted nonce to the authentication request /// messages except for stateless RPs in OpenID 1.x messages. /// public override MessageProtections RequiredProtection { // We actually manage to provide All protections regardless of OpenID version // on both the Provider and Relying Party side, except for stateless RPs for OpenID 1.x. get { return this.Version.Major < 2 ? MessageProtections.TamperProtection : MessageProtections.All; } } /// /// Gets or sets the message signature. /// /// Base 64 encoded signature calculated as specified in Section 6 (Generating Signatures). [MessagePart("openid.sig", IsRequired = true, AllowEmpty = false)] string ITamperResistantProtocolMessage.Signature { get; set; } /// /// Gets or sets the signed parameter order. /// /// Comma-separated list of signed fields. /// "op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce" /// /// This entry consists of the fields without the "openid." prefix that the signature covers. /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle", /// and if present in the response, "claimed_id" and "identity". /// Additional keys MAY be signed as part of the message. See Generating Signatures. /// [MessagePart("openid.signed", IsRequired = true, AllowEmpty = false)] string ITamperResistantOpenIdMessage.SignedParameterOrder { get; set; } /// /// Gets or sets the association handle used to sign the message. /// /// The handle for the association that was used to sign this assertion. [MessagePart("openid.assoc_handle", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] [MessagePart("openid.assoc_handle", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.None, MaxVersion = "1.1")] string ITamperResistantOpenIdMessage.AssociationHandle { get; set; } /// /// Gets or sets the nonce that will protect the message from replay attacks. /// string IReplayProtectedProtocolMessage.Nonce { get; set; } /// /// Gets the context within which the nonce must be unique. /// string IReplayProtectedProtocolMessage.NonceContext { get { if (this.ProviderEndpoint != null) { return this.ProviderEndpoint.AbsoluteUri; } else { // This is the Provider, on an OpenID 1.x check_authentication message. // We don't need any special nonce context because the Provider // generated and consumed the nonce. return string.Empty; } } } /// /// Gets or sets the UTC date/time the message was originally sent onto the network. /// /// /// The property setter should ensure a UTC date/time, /// and throw an exception if this is not possible. /// /// /// Thrown when a DateTime that cannot be converted to UTC is set. /// DateTime IExpiringProtocolMessage.UtcCreationDate { get { return this.creationDateUtc; } set { this.creationDateUtc = value.ToUniversalTimeSafe(); } } /// /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. /// /// If the Relying Party sent an invalid association handle with the request, it SHOULD be included here. /// /// For OpenID 1.1, we allow this to be present but empty to put up with poor implementations such as Blogger. /// [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = true, MaxVersion = "1.1")] [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] string ITamperResistantOpenIdMessage.InvalidateHandle { get; set; } /// /// Gets or sets the Provider Endpoint URI. /// [MessagePart("openid.op_endpoint", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] internal Uri ProviderEndpoint { get; set; } /// /// Gets or sets the return_to parameter as the relying party provided /// it in . /// /// Verbatim copy of the return_to URL parameter sent in the /// request, before the Provider modified it. [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, Encoder = typeof(OriginalStringUriEncoder))] internal Uri ReturnTo { get; set; } /// /// Gets or sets a value indicating whether the /// URI's query string is unaltered between when the Relying Party /// sent the original request and when the response was received. /// /// /// This property is not persisted in the transmitted message, and /// has no effect on the Provider-side of the communication. /// internal bool ReturnToParametersSignatureValidated { get; set; } /// /// Gets or sets the nonce that will protect the message from replay attacks. /// /// /// A string 255 characters or less in length, that MUST be unique to /// this particular successful authentication response. The nonce MUST start /// with the current time on the server, and MAY contain additional ASCII /// characters in the range 33-126 inclusive (printable non-whitespace characters), /// as necessary to make each response unique. The date and time MUST be /// formatted as specified in section 5.6 of [RFC3339] /// (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), /// with the following restrictions: /// /// All times must be in the UTC timezone, indicated with a "Z". /// No fractional seconds are allowed /// /// /// 2005-05-15T17:11:51ZUNIQUE internal string ResponseNonceTestHook { get { return this.ResponseNonce; } set { this.ResponseNonce = value; } } /// /// Gets or sets the nonce that will protect the message from replay attacks. /// /// /// A string 255 characters or less in length, that MUST be unique to /// this particular successful authentication response. The nonce MUST start /// with the current time on the server, and MAY contain additional ASCII /// characters in the range 33-126 inclusive (printable non-whitespace characters), /// as necessary to make each response unique. The date and time MUST be /// formatted as specified in section 5.6 of [RFC3339] /// (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), /// with the following restrictions: /// /// All times must be in the UTC timezone, indicated with a "Z". /// No fractional seconds are allowed /// /// /// 2005-05-15T17:11:51ZUNIQUE [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")] [MessagePart("openid.response_nonce", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] [MessagePart("openid.response_nonce", IsRequired = false, AllowEmpty = false, RequiredProtection = ProtectionLevel.None, MaxVersion = "1.1")] private string ResponseNonce { get { string uniqueFragment = ((IReplayProtectedProtocolMessage)this).Nonce; return this.creationDateUtc.ToString(PermissibleDateTimeFormats[0], CultureInfo.InvariantCulture) + uniqueFragment; } set { if (value == null) { ((IReplayProtectedProtocolMessage)this).Nonce = null; } else { int indexOfZ = value.IndexOf("Z", StringComparison.Ordinal); ErrorUtilities.VerifyProtocol(indexOfZ >= 0, MessagingStrings.UnexpectedMessagePartValue, Protocol.openid.response_nonce, value); this.creationDateUtc = DateTime.Parse(value.Substring(0, indexOfZ + 1), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); ((IReplayProtectedProtocolMessage)this).Nonce = value.Substring(indexOfZ + 1); } } } /// /// Gets the querystring key=value pairs in the return_to URL. /// private IDictionary ReturnToParameters { get { if (this.returnToParameters == null) { this.returnToParameters = HttpUtility.ParseQueryString(this.ReturnTo.Query).ToDictionary(); } return this.returnToParameters; } } /// /// Checks the message state for conformity to the protocol specification /// and throws an exception if the message is invalid. /// /// /// Some messages have required fields, or combinations of fields that must relate to each other /// in specialized ways. After deserializing a message, this method checks the state of the /// message to see if it conforms to the protocol. /// Note that this property should not check signatures or perform any state checks /// outside this scope of this particular message. /// /// Thrown if the message is invalid. public override void EnsureValidMessage() { base.EnsureValidMessage(); this.VerifyReturnToMatchesRecipient(); } /// /// Gets the value of a named parameter in the return_to URL without signature protection. /// /// The full name of the parameter whose value is being sought. /// The value of the parameter if it is present and unaltered from when /// the Relying Party signed it; null otherwise. /// /// This method will always return null on the Provider-side, since Providers /// cannot verify the private signature made by the relying party. /// internal string GetReturnToArgument(string key) { Requires.NotNullOrEmpty(key, "key"); ErrorUtilities.VerifyInternal(this.ReturnTo != null, "ReturnTo was expected to be required but is null."); string value; this.ReturnToParameters.TryGetValue(key, out value); return value; } /// /// Gets the names of the callback parameters added to the original authentication request /// without signature protection. /// /// A sequence of the callback parameter names. internal IEnumerable GetReturnToParameterNames() { return this.ReturnToParameters.Keys; } /// /// Gets a dictionary of all the message part names and values /// that are included in the message signature. /// /// The channel. /// /// A dictionary of the signed message parts. /// internal IDictionary GetSignedMessageParts(Channel channel) { Requires.NotNull(channel, "channel"); ITamperResistantOpenIdMessage signedSelf = this; if (signedSelf.SignedParameterOrder == null) { return EmptyDictionary.Instance; } MessageDictionary messageDictionary = channel.MessageDescriptions.GetAccessor(this); string[] signedPartNamesWithoutPrefix = signedSelf.SignedParameterOrder.Split(','); Dictionary signedParts = new Dictionary(signedPartNamesWithoutPrefix.Length); var signedPartNames = signedPartNamesWithoutPrefix.Select(part => Protocol.openid.Prefix + part); foreach (string partName in signedPartNames) { signedParts[partName] = messageDictionary[partName]; } return signedParts; } /// /// Determines whether one querystring contains every key=value pair that /// another querystring contains. /// /// The querystring that should contain at least all the key=value pairs of the other. /// The querystring containing the set of key=value pairs to test for in the other. /// /// true if contains all the query parameters that does; false otherwise. /// private static bool IsQuerySubsetOf(string superset, string subset) { NameValueCollection subsetArgs = HttpUtility.ParseQueryString(subset); NameValueCollection supersetArgs = HttpUtility.ParseQueryString(superset); return subsetArgs.Keys.Cast().All(key => string.Equals(subsetArgs[key], supersetArgs[key], StringComparison.Ordinal)); } /// /// Verifies that the openid.return_to field matches the URL of the actual HTTP request. /// /// /// From OpenId Authentication 2.0 section 11.1: /// To verify that the "openid.return_to" URL matches the URL that is processing this assertion: /// * The URL scheme, authority, and path MUST be the same between the two URLs. /// * Any query parameters that are present in the "openid.return_to" URL MUST /// also be present with the same values in the URL of the HTTP request the RP received. /// private void VerifyReturnToMatchesRecipient() { ErrorUtilities.VerifyProtocol( string.Equals(this.Recipient.Scheme, this.ReturnTo.Scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(this.Recipient.Authority, this.ReturnTo.Authority, StringComparison.OrdinalIgnoreCase) && string.Equals(this.Recipient.AbsolutePath, this.ReturnTo.AbsolutePath, StringComparison.Ordinal) && IsQuerySubsetOf(this.Recipient.Query, this.ReturnTo.Query), OpenIdStrings.ReturnToParamDoesNotMatchRequestUrl, Protocol.openid.return_to, this.ReturnTo, this.Recipient); } } }