diff options
18 files changed, 585 insertions, 11 deletions
diff --git a/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs b/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs index dd7b3d9..479816b 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs @@ -83,6 +83,10 @@ namespace DotNetOpenAuth.Test.OpenId.Messages { /// Verifies that the validation check throws if the return_to and the realm /// values are not compatible. /// </summary> + /// <remarks> + /// This test does not test all the realm-return_to matching rules as that is done in the Realm tests. + /// This test merely checks that the compatibility match occurs at all. + /// </remarks> [TestMethod, ExpectedException(typeof(ProtocolException))] public void RealmReturnToMismatchV2() { this.setupv2.Realm = "http://somehost/"; diff --git a/src/DotNetOpenAuth.Test/OpenId/Messages/IndirectErrorResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/Messages/IndirectErrorResponseTests.cs index 7794970..be72cb7 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Messages/IndirectErrorResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Messages/IndirectErrorResponseTests.cs @@ -18,7 +18,8 @@ namespace DotNetOpenAuth.Test.OpenId.Messages { [TestInitialize] public void Setup() { - this.response = new IndirectErrorResponse(Protocol.V20.Version, this.recipient); + CheckIdRequest request = new CheckIdRequest(Protocol.V20.Version, this.recipient, true); + this.response = new IndirectErrorResponse(request); } [TestMethod] diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 1ae0007..f4316d7 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -160,13 +160,19 @@ <Compile Include="OpenId\AssociationMemoryStore.cs" /> <Compile Include="OpenId\Associations.cs" /> <Compile Include="OpenId\ChannelElements\ITamperResistantOpenIdMessage.cs" /> + <Compile Include="OpenId\ChannelElements\OriginalStringUriEncoder.cs" /> <Compile Include="OpenId\ChannelElements\SigningBindingElement.cs" /> <Compile Include="OpenId\ChannelElements\KeyValueFormEncoding.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdChannel.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdMessageFactory.cs" /> <Compile Include="OpenId\Configuration.cs" /> <Compile Include="OpenId\Identifier.cs" /> + <Compile Include="OpenId\Messages\CheckAuthenticationRequest.cs" /> + <Compile Include="OpenId\Messages\CheckAuthenticationResponse.cs" /> <Compile Include="OpenId\Messages\CheckIdRequest.cs" /> + <Compile Include="OpenId\Messages\IndirectResponseBase.cs" /> + <Compile Include="OpenId\Messages\NegativeAssertionResponse.cs" /> + <Compile Include="OpenId\Messages\PositiveAssertionResponse.cs" /> <Compile Include="OpenId\NoDiscoveryIdentifier.cs" /> <Compile Include="OpenId\Realm.cs" /> <Compile Include="OpenId\RelyingPartyDescription.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs b/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs index 51a888e..f1c39f5 100644 --- a/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs +++ b/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs @@ -25,6 +25,7 @@ namespace DotNetOpenAuth.Messaging { public MessagePartAttribute() { this.AllowEmpty = true; this.MinVersionValue = new Version(0, 0); + this.MaxVersionValue = new Version(int.MaxValue, 0); } /// <summary> @@ -34,7 +35,8 @@ namespace DotNetOpenAuth.Messaging { /// A special name to give the value of this member in the serialized message. /// When null or empty, the name of the member will be used in the serialized message. /// </param> - public MessagePartAttribute(string name) : this() { + public MessagePartAttribute(string name) + : this() { this.Name = name; } @@ -63,7 +65,8 @@ namespace DotNetOpenAuth.Messaging { public bool AllowEmpty { get; set; } /// <summary> - /// Gets or sets a custom encoder to use to translate the applied member to and from a string. + /// Gets or sets an IMessagePartEncoder custom encoder to use + /// to translate the applied member to and from a string. /// </summary> public Type Encoder { get; set; } @@ -78,10 +81,35 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets or sets the maximum version of the protocol this attribute applies to. + /// </summary> + /// <value>Defaults to int.MaxValue for the major version number.</value> + /// <remarks> + /// Specifying <see cref="MinVersion"/> on another attribute on the same member + /// automatically turns this attribute off. This property should only be set when + /// a property is totally dropped from a newer version of the protocol. + /// </remarks> + public string MaxVersion { + get { return this.MaxVersionValue.ToString(); } + set { this.MaxVersionValue = new Version(value); } + } + + /// <summary> /// Gets or sets the minimum version of the protocol this attribute applies to /// and overrides any attributes with lower values for this property. /// </summary> /// <value>Defaults to 0.0.</value> internal Version MinVersionValue { get; set; } + + /// <summary> + /// Gets or sets the maximum version of the protocol this attribute applies to. + /// </summary> + /// <value>Defaults to int.MaxValue for the major version number.</value> + /// <remarks> + /// Specifying <see cref="MinVersion"/> on another attribute on the same member + /// automatically turns this attribute off. This property should only be set when + /// a property is totally dropped from a newer version of the protocol. + /// </remarks> + internal Version MaxVersionValue { get; set; } } } diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs index 94812d8..a363373 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs @@ -100,6 +100,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { (from a in member.GetCustomAttributes(typeof(MessagePartAttribute), true).OfType<MessagePartAttribute>() orderby a.MinVersionValue descending where a.MinVersionValue <= this.messageTypeAndVersion.Version + where a.MaxVersionValue >= this.messageTypeAndVersion.Version select a).FirstOrDefault(); if (partAttribute != null) { MessagePart part = new MessagePart(member, partAttribute); diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs index 3c0ecad..533e818 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs @@ -10,17 +10,25 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; /// <summary> /// An interface that OAuth messages implement to support signing. /// </summary> - internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage { + internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage, IReplayProtectedProtocolMessage { /// <summary> /// Gets or sets the association handle used to sign the message. /// </summary> + /// <value>The handle for the association that was used to sign this assertion. </value> string AssociationHandle { get; set; } /// <summary> + /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. + /// </summary> + /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value> + string InvalidateHandle { get; set; } + + /// <summary> /// Gets or sets the signed parameter order. /// </summary> /// <value>Comma-separated list of signed fields.</value> @@ -31,6 +39,6 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// and if present in the response, "claimed_id" and "identity". /// Additional keys MAY be signed as part of the message. See Generating Signatures. /// </remarks> - string SignedParameterOrder { get; set; } + string SignedParameterOrder { get; set; } // TODO: make sure we have a unit test to verify that an incoming message with fewer signed fields than required will be rejected. } } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OriginalStringUriEncoder.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OriginalStringUriEncoder.cs new file mode 100644 index 0000000..75b01e1 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OriginalStringUriEncoder.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="OriginalStringUriEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// A Uri encoder that serializes using <see cref="Uri.OriginalString"/> + /// rather than the standard <see cref="Uri.AbsoluteUri"/>. + /// </summary> + internal class OriginalStringUriEncoder : IMessagePartEncoder { + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + Uri uriValue = (Uri)value; + return uriValue != null ? uriValue.OriginalString : null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return value != null ? new Uri(value) : null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs index 621054f..bd68111 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs @@ -24,7 +24,7 @@ namespace DotNetOpenAuth.OpenId.Messages { /// <param name="version">The OpenID version this message must comply with.</param> /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> protected AssociateRequest(Version version, Uri providerEndpoint) - : base(version, providerEndpoint, "associate", MessageTransport.Direct) { + : base(version, providerEndpoint, GetProtocolConstant(version, p => p.Args.Mode.associate), MessageTransport.Direct) { } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs new file mode 100644 index 0000000..2a62fc1 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckAuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// A message a Relying Party sends to a Provider to confirm the validity + /// of a positive assertion that was signed by a Provider-only secret. + /// </summary> + /// <remarks> + /// The significant payload of this message depends entirely upon the + /// assertion message, and therefore is all in the + /// <see cref="DotNetOpenAuth.Messaging.IProtocolMessage.ExtraData"/> property bag. + /// </remarks> + internal class CheckAuthenticationRequest : RequestBase { + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + internal CheckAuthenticationRequest(Version version, Uri providerEndpoint) + : base(version, providerEndpoint, GetProtocolConstant(version, p => p.Args.Mode.check_authentication), DotNetOpenAuth.Messaging.MessageTransport.Direct) { + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs new file mode 100644 index 0000000..d66d0a9 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The message sent from the Provider to the Relying Party to confirm/deny + /// the validity of an assertion that was signed by a private Provider secret. + /// </summary> + internal class CheckAuthenticationResponse : DirectResponseBase { + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationResponse"/> class. + /// </summary> + /// <param name="request">The request that this message is responding to.</param> + internal CheckAuthenticationResponse(CheckAuthenticationRequest request) + : base(request) { + } + + /// <summary> + /// Gets or sets a value indicating whether the signature of the verification request is valid. + /// </summary> + [MessagePart("is_valid", IsRequired = true)] + internal bool IsValid { get; set; } + + /// <summary> + /// Gets or sets the handle the relying party should invalidate if <see cref="IsValid"/> is true. + /// </summary> + /// <value>The "invalidate_handle" value sent in the verification request, if the OP confirms it is invalid.</value> + /// <remarks> + /// <para>If present in a verification response with "is_valid" set to "true", + /// the Relying Party SHOULD remove the corresponding association from + /// its store and SHOULD NOT send further authentication requests with + /// this handle.</para> + /// <para>This two-step process for invalidating associations is necessary + /// to prevent an attacker from invalidating an association at will by + /// adding "invalidate_handle" parameters to an authentication response.</para> + /// </remarks> + [MessagePart("invalidate_handle", IsRequired = false, AllowEmpty = false)] + internal string InvalidateHandle { get; set; } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs index 46a32f6..bdc3f48 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs @@ -34,6 +34,15 @@ namespace DotNetOpenAuth.OpenId.Messages { } /// <summary> + /// Gets a value indicating whether the Provider is allowed to interact with the user + /// as part of authentication. + /// </summary> + /// <value><c>true</c> if using OpenID immediate mode; otherwise, <c>false</c>.</value> + internal bool Immediate { + get { return String.Equals(this.Mode, Protocol.Args.Mode.checkid_immediate, StringComparison.Ordinal); } + } + + /// <summary> /// Gets or sets the Claimed Identifier. /// </summary> /// <remarks> @@ -123,6 +132,7 @@ namespace DotNetOpenAuth.OpenId.Messages { base.EnsureValidMessage(); if (this.ProtocolVersion.Major >= 2) { + // Ensure that claimed_id and identity are either both present or both absent. ErrorUtilities.VerifyProtocol((this.ClaimedIdentifier == null) == (this.LocalIdentifier == null), OpenIdStrings.ClaimedIdAndLocalIdMustBothPresentOrAbsent); } diff --git a/src/DotNetOpenAuth/OpenId/Messages/IndirectErrorResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/IndirectErrorResponse.cs index 4b038b5..ffc73f8 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/IndirectErrorResponse.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/IndirectErrorResponse.cs @@ -17,14 +17,13 @@ namespace DotNetOpenAuth.OpenId.Messages { /// <remarks> /// This class satisfies OpenID 2.0 section 5.2.3. /// </remarks> - internal class IndirectErrorResponse : RequestBase { + internal class IndirectErrorResponse : IndirectResponseBase { /// <summary> /// Initializes a new instance of the <see cref="IndirectErrorResponse"/> class. /// </summary> - /// <param name="version">The OpenID version this message must comply with.</param> - /// <param name="relyingPartyReturnTo">The value of the Relying Party's openid.return_to argument.</param> - internal IndirectErrorResponse(Version version, Uri relyingPartyReturnTo) - : base(version, relyingPartyReturnTo, "error", MessageTransport.Indirect) { + /// <param name="request">The request that resulted in this error on the Provider.</param> + internal IndirectErrorResponse(CheckIdRequest request) + : base(request, "error") { } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Messages/IndirectResponseBase.cs b/src/DotNetOpenAuth/OpenId/Messages/IndirectResponseBase.cs new file mode 100644 index 0000000..bcda594 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/IndirectResponseBase.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// <copyright file="IndirectResponseBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A common base class from which indirect response messages should derive. + /// </summary> + internal class IndirectResponseBase : RequestBase { + /// <summary> + /// Initializes a new instance of the <see cref="IndirectResponseBase"/> class. + /// </summary> + /// <param name="request">The request that caused this response message to be constructed.</param> + /// <param name="mode">The value of the openid.mode parameter.</param> + protected IndirectResponseBase(CheckIdRequest request, string mode) + : base(GetVersion(request), GetReturnTo(request), mode, MessageTransport.Indirect) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + this.OriginatingRequest = request; + } + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectResponseBase"/> class + /// for unsolicited assertion scenarios. + /// </summary> + /// <param name="version">The OpenID version supported at the Relying Party.</param> + /// <param name="relyingPartyReturnTo"> + /// The URI at which the Relying Party receives OpenID indirect messages. + /// </param> + /// <param name="mode">The value to use for the openid.mode parameter.</param> + protected IndirectResponseBase(Version version, Uri relyingPartyReturnTo, string mode) + : base(version, relyingPartyReturnTo, mode, MessageTransport.Indirect) { + } + + /// <summary> + /// Gets the originating request message, if applicable. + /// </summary> + protected CheckIdRequest OriginatingRequest { get; private set; } + + /// <summary> + /// Gets the <see cref="IProtocolMessage.ProtocolVersion"/> property of a message. + /// </summary> + /// <param name="message">The message to fetch the protocol version from.</param> + /// <returns>The value of the <see cref="IProtocolMessage.ProtocolVersion"/> property.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + protected static Version GetVersion(IProtocolMessage message) { + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + return message.ProtocolVersion; + } + + /// <summary> + /// Gets the <see cref="CheckIdRequest.ReturnTo"/> property of a message. + /// </summary> + /// <param name="message">The message to fetch the ReturnTo from.</param> + /// <returns>The value of the <see cref="CheckIdRequest.ReturnTo"/> property.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + private static Uri GetReturnTo(CheckIdRequest message) { + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + return message.ReturnTo; + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/NegativeAssertionResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/NegativeAssertionResponse.cs new file mode 100644 index 0000000..b0543e9 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/NegativeAssertionResponse.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="NegativeAssertionResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The message OpenID Providers send back to Relying Parties to refuse + /// to assert the identity of a user. + /// </summary> + internal class NegativeAssertionResponse : IndirectResponseBase { + /// <summary> + /// Initializes a new instance of the <see cref="NegativeAssertionResponse"/> class. + /// </summary> + /// <param name="request">The request that the relying party sent.</param> + internal NegativeAssertionResponse(CheckIdRequest request) + : base(request, GetMode(request)) { + } + + /// <summary> + /// Gets or sets the URL the relying party can use to upgrade their authentication + /// request from an immediate to a setup message. + /// </summary> + /// <value>URL to redirect User-Agent to so the End User can do whatever's necessary to fulfill the assertion.</value> + /// <remarks> + /// This part is only included + /// </remarks> + [MessagePart("openid.user_setup_url", AllowEmpty = false, IsRequired = false, MaxVersion = "1.1")] + internal Uri UserSetupUrl { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>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.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (OriginatingRequest.Immediate && Protocol.Version.Major < 2) { + ErrorUtilities.VerifyProtocol(this.UserSetupUrl != null, OpenIdStrings.UserSetupUrlRequiredInImmediateNegativeResponse); + } + } + + /// <summary> + /// Gets the value for the openid.mode that is appropriate for this response. + /// </summary> + /// <param name="request">The request that we're responding to.</param> + /// <returns>The value of the openid.mode parameter to use.</returns> + private static string GetMode(CheckIdRequest request) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + Protocol protocol = Protocol.Lookup(request.ProtocolVersion); + return request.Immediate ? protocol.Args.Mode.setup_needed : protocol.Args.Mode.cancel; + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/PositiveAssertionResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/PositiveAssertionResponse.cs new file mode 100644 index 0000000..4f45cee --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/PositiveAssertionResponse.cs @@ -0,0 +1,216 @@ +//----------------------------------------------------------------------- +// <copyright file="PositiveAssertionResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// An identity assertion from a Provider to a Relying Party, stating that the + /// user operating the user agent is in fact some specific user known to the Provider. + /// </summary> + [DebuggerDisplay("OpenID {ProtocolVersion} {Mode} {LocalIdentifier}")] + internal class PositiveAssertionResponse : IndirectResponseBase, ITamperResistantOpenIdMessage { + /// <summary> + /// The allowed date/time formats for the response_nonce parameter. + /// </summary> + /// <remarks> + /// This array of formats is not yet a complete list. + /// </remarks> + private static readonly string[] PermissibleDateTimeFormats = { "yyyy-MM-ddTHH:mm:ssZ" }; + + /// <summary> + /// Backing field for the <see cref="IExpiringProtocolMessage.UtcCreationDate"/> property. + /// </summary> + private DateTime creationDateUtc; + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAssertionResponse"/> class. + /// </summary> + /// <param name="request"> + /// The authentication request that caused this assertion to be generated. + /// </param> + internal PositiveAssertionResponse(CheckIdRequest request) + : base(request, Protocol.Lookup(GetVersion(request)).Args.Mode.id_res) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + this.ReturnTo = request.ReturnTo; + } + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAssertionResponse"/> class + /// for unsolicited assertions. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="relyingPartyReturnTo">The return_to URL of the Relying Party. + /// This value will commonly be from <see cref="CheckIdRequest.ReturnTo"/>, + /// but for unsolicited assertions may come from the Provider performing RP discovery + /// to find the appropriate return_to URL to use.</param> + internal PositiveAssertionResponse(Version version, Uri relyingPartyReturnTo) + : base(version, relyingPartyReturnTo, Protocol.Lookup(version).Args.Mode.id_res) { + } + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + /// <value>Base 64 encoded signature calculated as specified in Section 6 (Generating Signatures).</value> + [MessagePart("openid.sig", IsRequired = true, AllowEmpty = false)] + string ITamperResistantProtocolMessage.Signature { get; set; } + + /// <summary> + /// Gets or sets the signed parameter order. + /// </summary> + /// <value>Comma-separated list of signed fields.</value> + /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example> + /// <remarks> + /// 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. + /// </remarks> + [MessagePart("openid.signed", IsRequired = true, AllowEmpty = false)] + string ITamperResistantOpenIdMessage.SignedParameterOrder { get; set; } + + /// <summary> + /// Gets or sets the association handle used to sign the message. + /// </summary> + /// <value>The handle for the association that was used to sign this assertion. </value> + [MessagePart("openid.assoc_handle", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign)] + string ITamperResistantOpenIdMessage.AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + string IReplayProtectedProtocolMessage.Nonce { get; set; } + + /// <summary> + /// Gets or sets the UTC date/time the message was originally sent onto the network. + /// </summary> + /// <remarks> + /// The property setter should ensure a UTC date/time, + /// and throw an exception if this is not possible. + /// </remarks> + /// <exception cref="ArgumentException"> + /// Thrown when a DateTime that cannot be converted to UTC is set. + /// </exception> + DateTime IExpiringProtocolMessage.UtcCreationDate { + get { return this.creationDateUtc; } + set { this.creationDateUtc = value.ToUniversalTime(); } + } + + /// <summary> + /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. + /// </summary> + /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value> + [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = false)] + string ITamperResistantOpenIdMessage.InvalidateHandle { get; set; } + + /// <summary> + /// Gets or sets the Provider Endpoint URI. + /// </summary> + [MessagePart("openid.op_endpoint", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + internal Uri ProviderEndpoint { get; set; } + + /// <summary> + /// Gets or sets the Claimed Identifier. + /// </summary> + /// <remarks> + /// <para>"openid.claimed_id" and "openid.identity" SHALL be either both present or both absent. + /// If neither value is present, the assertion is not about an identifier, + /// and will contain other information in its payload, using extensions (Extensions). </para> + /// </remarks> + [MessagePart("openid.claimed_id", IsRequired = false, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + internal Identifier ClaimedIdentifier { get; set; } + + /// <summary> + /// Gets or sets the OP Local Identifier. + /// </summary> + /// <value>The OP-Local Identifier. </value> + /// <remarks> + /// <para>OpenID Providers MAY assist the end user in selecting the Claimed + /// and OP-Local Identifiers about which the assertion is made. + /// The openid.identity field MAY be omitted if an extension is in use that + /// makes the response meaningful without it (see openid.claimed_id above). </para> + /// </remarks> + [MessagePart("openid.identity", IsRequired = false, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign)] + internal Identifier LocalIdentifier { get; set; } + + /// <summary> + /// Gets or sets the return_to parameter as the relying party provided + /// it in <see cref="CheckIdRequest.ReturnTo"/>. + /// </summary> + /// <value>Verbatim copy of the return_to URL parameter sent in the + /// request, before the Provider modified it. </value> + [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, Encoder = typeof(OriginalStringUriEncoder))] + internal Uri ReturnTo { get; set; } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + /// <value> + /// <para>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:</para> + /// <list type="bullet"> + /// <item>All times must be in the UTC timezone, indicated with a "Z".</item> + /// <item>No fractional seconds are allowed</item> + /// </list> + /// </value> + /// <example>2005-05-15T17:11:51ZUNIQUE</example> + [MessagePart("openid.response_nonce", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + 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); + ((IReplayProtectedProtocolMessage)this).Nonce = value.Substring(indexOfZ + 1); + } + } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>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.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.ProtocolVersion.Major >= 2) { + // Ensure that claimed_id and identity are either both present or both absent. + ErrorUtilities.VerifyProtocol((this.ClaimedIdentifier == null) == (this.LocalIdentifier == null), OpenIdStrings.ClaimedIdAndLocalIdMustBothPresentOrAbsent); + } + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs index bc00ac0..28e53b7 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs @@ -169,5 +169,20 @@ namespace DotNetOpenAuth.OpenId.Messages { internal void SetAsIncoming() { this.incoming = true; } + + /// <summary> + /// Gets some string from a given version of the OpenID protocol. + /// </summary> + /// <param name="protocolVersion">The protocol version to use for lookup.</param> + /// <param name="mode">A function that can retrieve the desired protocol constant.</param> + /// <returns>The value of the constant.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + protected static string GetProtocolConstant(Version protocolVersion, Func<Protocol, string> mode) { + ErrorUtilities.VerifyArgumentNotNull(protocolVersion, "protocolVersion"); + return mode(Protocol.Lookup(protocolVersion)); + } } } diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index d51ec37..a3d6255 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -232,6 +232,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to The openid.user_setup_url parameter is required when sending negative assertion messages in response to immediate mode requests.. + /// </summary> + internal static string UserSetupUrlRequiredInImmediateNegativeResponse { + get { + return ResourceManager.GetString("UserSetupUrlRequiredInImmediateNegativeResponse", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to XRI resolution failed.. /// </summary> internal static string XriResolutionFailed { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index fd3d799..4ba06e2 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -174,6 +174,9 @@ <data name="ReturnToNotUnderRealm" xml:space="preserve"> <value>return_to '{0}' not under realm '{1}'.</value> </data> + <data name="UserSetupUrlRequiredInImmediateNegativeResponse" xml:space="preserve"> + <value>The openid.user_setup_url parameter is required when sending negative assertion messages in response to immediate mode requests.</value> + </data> <data name="XriResolutionFailed" xml:space="preserve"> <value>XRI resolution failed.</value> </data> |