//-----------------------------------------------------------------------
//
// 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);
}
}
}