summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider')
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AnonymousRequest.cs92
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs95
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationRelyingPartyType.cs26
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AuthenticationRequest.cs227
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AutoResponsiveRequest.cs78
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/AXFetchAsSregTransform.cs87
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/GsaIcamProfile.cs193
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/PpidGeneration.cs122
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/ExtensionsInteropHelper.cs162
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/UI/UIRequestTools.cs68
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HmacShaAssociationProvider.cs71
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HostProcessedRequest.cs182
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IAnonymousRequest.cs25
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs83
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IErrorReporting.cs43
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IProviderAssociationStore.cs90
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProvider.cs674
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProviderUtilities.cs70
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs223
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs84
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationKeyStorage.cs79
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Request.cs210
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/RequestContract.cs48
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/StandardProviderApplicationStore.cs117
24 files changed, 3149 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AnonymousRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AnonymousRequest.cs
new file mode 100644
index 0000000..581d39e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AnonymousRequest.cs
@@ -0,0 +1,92 @@
+//-----------------------------------------------------------------------
+// <copyright file="AnonymousRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Provides access to a host Provider to read an incoming extension-only checkid request message,
+ /// and supply extension responses or a cancellation message to the RP.
+ /// </summary>
+ [Serializable]
+ internal class AnonymousRequest : HostProcessedRequest, IAnonymousRequest {
+ /// <summary>
+ /// The extension-response message to send, if the host site chooses to send it.
+ /// </summary>
+ private readonly IndirectSignedResponse positiveResponse;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AnonymousRequest"/> class.
+ /// </summary>
+ /// <param name="provider">The provider that received the request.</param>
+ /// <param name="request">The incoming authentication request message.</param>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AuthenticationRequest", Justification = "Type name"), SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code contracts require it.")]
+ internal AnonymousRequest(OpenIdProvider provider, SignedResponseRequest request)
+ : base(provider, request) {
+ Requires.NotNull(provider, "provider");
+ Requires.True(!(request is CheckIdRequest), "request");
+
+ this.positiveResponse = new IndirectSignedResponse(request);
+ }
+
+ #region HostProcessedRequest members
+
+ /// <summary>
+ /// Gets or sets the provider endpoint.
+ /// </summary>
+ /// <value>
+ /// The default value is the URL that the request came in on from the relying party.
+ /// </value>
+ public override Uri ProviderEndpoint {
+ get { return this.positiveResponse.ProviderEndpoint; }
+ set { this.positiveResponse.ProviderEndpoint = value; }
+ }
+
+ #endregion
+
+ #region IAnonymousRequest Members
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user approved sending any data to the relying party.
+ /// </summary>
+ /// <value><c>true</c> if approved; otherwise, <c>false</c>.</value>
+ public bool? IsApproved { get; set; }
+
+ #endregion
+
+ #region Request members
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be sent to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// This property returns false if there are properties that must be set on this
+ /// request instance before the response can be sent.
+ /// </remarks>
+ public override bool IsResponseReady {
+ get { return this.IsApproved.HasValue; }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ protected override IProtocolMessage ResponseMessage {
+ get {
+ if (this.IsApproved.HasValue) {
+ return this.IsApproved.Value ? (IProtocolMessage)this.positiveResponse : this.NegativeResponse;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs
new file mode 100644
index 0000000..bf3d909
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs
@@ -0,0 +1,95 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociationDataBag.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// A signed and encrypted serialization of an association.
+ /// </summary>
+ internal class AssociationDataBag : DataBag, IStreamSerializingDataBag {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssociationDataBag"/> class.
+ /// </summary>
+ public AssociationDataBag() {
+ }
+
+ /// <summary>
+ /// Gets or sets the association secret.
+ /// </summary>
+ [MessagePart(IsRequired = true)]
+ internal byte[] Secret { get; set; }
+
+ /// <summary>
+ /// Gets or sets the UTC time that this association expires.
+ /// </summary>
+ [MessagePart(IsRequired = true)]
+ internal DateTime ExpiresUtc { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is for "dumb" mode RPs.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is private association; otherwise, <c>false</c>.
+ /// </value>
+ [MessagePart(IsRequired = true)]
+ internal bool IsPrivateAssociation {
+ get { return this.AssociationType == AssociationRelyingPartyType.Dumb; }
+ set { this.AssociationType = value ? AssociationRelyingPartyType.Dumb : AssociationRelyingPartyType.Smart; }
+ }
+
+ /// <summary>
+ /// Gets or sets the type of the association (shared or private, a.k.a. smart or dumb).
+ /// </summary>
+ internal AssociationRelyingPartyType AssociationType { get; set; }
+
+ /// <summary>
+ /// Serializes the instance to the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ public void Serialize(Stream stream) {
+ var writer = new BinaryWriter(stream);
+ writer.Write(this.IsPrivateAssociation);
+ writer.WriteBuffer(this.Secret);
+ writer.Write((int)(this.ExpiresUtc - TimestampEncoder.Epoch).TotalSeconds);
+ writer.Flush();
+ }
+
+ /// <summary>
+ /// Initializes the fields on this instance from the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ public void Deserialize(Stream stream) {
+ var reader = new BinaryReader(stream);
+ this.IsPrivateAssociation = reader.ReadBoolean();
+ this.Secret = reader.ReadBuffer();
+ this.ExpiresUtc = TimestampEncoder.Epoch + TimeSpan.FromSeconds(reader.ReadInt32());
+ }
+
+ /// <summary>
+ /// Creates the formatter used for serialization of this type.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
+ /// <param name="minimumAge">The minimum age.</param>
+ /// <returns>
+ /// A formatter for serialization.
+ /// </returns>
+ internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan? minimumAge = null) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Contract.Ensures(Contract.Result<IDataBagFormatter<AssociationDataBag>>() != null);
+ return new BinaryDataBagFormatter<AssociationDataBag>(cryptoKeyStore, bucket, signed: true, encrypted: true, minimumAge: minimumAge);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationRelyingPartyType.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationRelyingPartyType.cs
new file mode 100644
index 0000000..4d121b1
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationRelyingPartyType.cs
@@ -0,0 +1,26 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociationRelyingPartyType.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ /// <summary>
+ /// An enumeration that can specify how a given <see cref="Association"/> is used.
+ /// </summary>
+ public enum AssociationRelyingPartyType {
+ /// <summary>
+ /// The <see cref="Association"/> manages a shared secret between
+ /// Provider and Relying Party sites that allows the RP to verify
+ /// the signature on a message from an OP.
+ /// </summary>
+ Smart,
+
+ /// <summary>
+ /// The <see cref="Association"/> manages a secret known alone by
+ /// a Provider that allows the Provider to verify its own signatures
+ /// for "dumb" (stateless) relying parties.
+ /// </summary>
+ Dumb
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AuthenticationRequest.cs
new file mode 100644
index 0000000..09b1073
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AuthenticationRequest.cs
@@ -0,0 +1,227 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthenticationRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Implements the <see cref="IAuthenticationRequest"/> interface
+ /// so that OpenID Provider sites can easily respond to authentication
+ /// requests.
+ /// </summary>
+ [Serializable]
+ internal class AuthenticationRequest : HostProcessedRequest, IAuthenticationRequest {
+ /// <summary>
+ /// The positive assertion to send, if the host site chooses to send it.
+ /// </summary>
+ private readonly PositiveAssertionResponse positiveResponse;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
+ /// </summary>
+ /// <param name="provider">The provider that received the request.</param>
+ /// <param name="request">The incoming authentication request message.</param>
+ internal AuthenticationRequest(OpenIdProvider provider, CheckIdRequest request)
+ : base(provider, request) {
+ Requires.NotNull(provider, "provider");
+
+ this.positiveResponse = new PositiveAssertionResponse(request);
+
+ if (this.ClaimedIdentifier == Protocol.ClaimedIdentifierForOPIdentifier &&
+ Protocol.ClaimedIdentifierForOPIdentifier != null) {
+ // Force the hosting OP to deal with identifier_select by nulling out the two identifiers.
+ this.IsDirectedIdentity = true;
+ this.positiveResponse.ClaimedIdentifier = null;
+ this.positiveResponse.LocalIdentifier = null;
+ }
+
+ // URL delegation is only detectable from 2.0 RPs, since openid.claimed_id isn't included from 1.0 RPs.
+ // If the openid.claimed_id is present, and if it's different than the openid.identity argument, then
+ // the RP has discovered a claimed identifier that has delegated authentication to this Provider.
+ this.IsDelegatedIdentifier = this.ClaimedIdentifier != null && this.ClaimedIdentifier != this.LocalIdentifier;
+
+ Reporting.RecordEventOccurrence("AuthenticationRequest.IsDelegatedIdentifier", this.IsDelegatedIdentifier.ToString());
+ }
+
+ #region HostProcessedRequest members
+
+ /// <summary>
+ /// Gets or sets the provider endpoint.
+ /// </summary>
+ /// <value>
+ /// The default value is the URL that the request came in on from the relying party.
+ /// </value>
+ public override Uri ProviderEndpoint {
+ get { return this.positiveResponse.ProviderEndpoint; }
+ set { this.positiveResponse.ProviderEndpoint = value; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be created and sent.
+ /// </summary>
+ public override bool IsResponseReady {
+ get {
+ // The null checks on the identifiers is to make sure that an identifier_select
+ // has been resolved to actual identifiers.
+ return this.IsAuthenticated.HasValue &&
+ (!this.IsAuthenticated.Value || !this.IsDirectedIdentity || (this.LocalIdentifier != null && this.ClaimedIdentifier != null));
+ }
+ }
+
+ #region IAuthenticationRequest Properties
+
+ /// <summary>
+ /// Gets a value indicating whether the Provider should help the user
+ /// select a Claimed Identifier to send back to the relying party.
+ /// </summary>
+ public bool IsDirectedIdentity { get; private set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the requesting Relying Party is using a delegated URL.
+ /// </summary>
+ /// <remarks>
+ /// When delegated identifiers are used, the <see cref="ClaimedIdentifier"/> should not
+ /// be changed at the Provider during authentication.
+ /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties.
+ /// A relying party implementing only OpenID 1.x may use delegation and this property will
+ /// return false anyway.
+ /// </remarks>
+ public bool IsDelegatedIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the Local Identifier to this OpenID Provider of the user attempting
+ /// to authenticate. Check <see cref="IsDirectedIdentity"/> to see if
+ /// this value is valid.
+ /// </summary>
+ /// <remarks>
+ /// This may or may not be the same as the Claimed Identifier that the user agent
+ /// originally supplied to the relying party. The Claimed Identifier
+ /// endpoint may be delegating authentication to this provider using
+ /// this provider's local id, which is what this property contains.
+ /// Use this identifier when looking up this user in the provider's user account
+ /// list.
+ /// </remarks>
+ public Identifier LocalIdentifier {
+ get {
+ return this.positiveResponse.LocalIdentifier;
+ }
+
+ set {
+ // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity.
+ if (this.IsDirectedIdentity) {
+ if (this.ClaimedIdentifier != null && this.ClaimedIdentifier != value) {
+ throw new InvalidOperationException(OpenIdStrings.IdentifierSelectRequiresMatchingIdentifiers);
+ }
+
+ this.positiveResponse.ClaimedIdentifier = value;
+ }
+
+ this.positiveResponse.LocalIdentifier = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the identifier that the user agent is claiming at the relying party site.
+ /// Check <see cref="IsDirectedIdentity"/> to see if this value is valid.
+ /// </summary>
+ /// <remarks>
+ /// <para>This property can only be set if <see cref="IsDelegatedIdentifier"/> is
+ /// false, to prevent breaking URL delegation.</para>
+ /// <para>This will not be the same as this provider's local identifier for the user
+ /// if the user has set up his/her own identity page that points to this
+ /// provider for authentication.</para>
+ /// <para>The provider may use this identifier for displaying to the user when
+ /// asking for the user's permission to authenticate to the relying party.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown from the setter
+ /// if <see cref="IsDelegatedIdentifier"/> is true.</exception>
+ public Identifier ClaimedIdentifier {
+ get {
+ return this.positiveResponse.ClaimedIdentifier;
+ }
+
+ set {
+ // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity.
+ if (this.IsDirectedIdentity) {
+ this.positiveResponse.LocalIdentifier = value;
+ }
+
+ this.positiveResponse.ClaimedIdentifier = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the provider has determined that the
+ /// <see cref="ClaimedIdentifier"/> belongs to the currently logged in user
+ /// and wishes to share this information with the consumer.
+ /// </summary>
+ public bool? IsAuthenticated { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the original request message.
+ /// </summary>
+ protected new CheckIdRequest RequestMessage {
+ get { return (CheckIdRequest)base.RequestMessage; }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ protected override IProtocolMessage ResponseMessage {
+ get {
+ if (this.IsAuthenticated.HasValue) {
+ return this.IsAuthenticated.Value ? (IProtocolMessage)this.positiveResponse : this.NegativeResponse;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ #region IAuthenticationRequest Methods
+
+ /// <summary>
+ /// Adds an optional fragment (#fragment) portion to the ClaimedIdentifier.
+ /// Useful for identifier recycling.
+ /// </summary>
+ /// <param name="fragment">Should not include the # prefix character as that will be added internally.
+ /// May be null or the empty string to clear a previously set fragment.</param>
+ /// <remarks>
+ /// <para>Unlike the <see cref="ClaimedIdentifier"/> property, which can only be set if
+ /// using directed identity, this method can be called on any URI claimed identifier.</para>
+ /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled,
+ /// this method should<i>not</i> be called for XRIs.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">
+ /// Thrown when this method is called on an XRI, or on a directed identity
+ /// request before the <see cref="ClaimedIdentifier"/> property is set.
+ /// </exception>
+ public void SetClaimedIdentifierFragment(string fragment) {
+ UriBuilder builder = new UriBuilder(this.ClaimedIdentifier);
+ builder.Fragment = fragment;
+ this.positiveResponse.ClaimedIdentifier = builder.Uri;
+ }
+
+ /// <summary>
+ /// Sets the Claimed and Local identifiers even after they have been initially set.
+ /// </summary>
+ /// <param name="identifier">The value to set to the <see cref="ClaimedIdentifier"/> and <see cref="LocalIdentifier"/> properties.</param>
+ internal void ResetClaimedAndLocalIdentifiers(Identifier identifier) {
+ Requires.NotNull(identifier, "identifier");
+
+ this.positiveResponse.ClaimedIdentifier = identifier;
+ this.positiveResponse.LocalIdentifier = identifier;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AutoResponsiveRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AutoResponsiveRequest.cs
new file mode 100644
index 0000000..0d98e67
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AutoResponsiveRequest.cs
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------
+// <copyright file="AutoResponsiveRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Handles messages coming into an OpenID Provider for which the entire
+ /// response message can be automatically determined without help from
+ /// the hosting web site.
+ /// </summary>
+ internal class AutoResponsiveRequest : Request {
+ /// <summary>
+ /// The response message to send.
+ /// </summary>
+ private readonly IProtocolMessage response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AutoResponsiveRequest"/> class.
+ /// </summary>
+ /// <param name="request">The request message.</param>
+ /// <param name="response">The response that is ready for transmittal.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal AutoResponsiveRequest(IDirectedProtocolMessage request, IProtocolMessage response, ProviderSecuritySettings securitySettings)
+ : base(request, securitySettings) {
+ Requires.NotNull(response, "response");
+
+ this.response = response;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AutoResponsiveRequest"/> class
+ /// for a response to an unrecognizable request.
+ /// </summary>
+ /// <param name="response">The response that is ready for transmittal.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal AutoResponsiveRequest(IProtocolMessage response, ProviderSecuritySettings securitySettings)
+ : base(IndirectResponseBase.GetVersion(response), securitySettings) {
+ Requires.NotNull(response, "response");
+
+ this.response = response;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be sent to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// This property returns false if there are properties that must be set on this
+ /// request instance before the response can be sent.
+ /// </remarks>
+ public override bool IsResponseReady {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ internal IProtocolMessage ResponseMessageTestHook {
+ get { return this.ResponseMessage; }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ protected override IProtocolMessage ResponseMessage {
+ get { return this.response; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/AXFetchAsSregTransform.cs
new file mode 100644
index 0000000..3a72c5e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/AXFetchAsSregTransform.cs
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------
+// <copyright file="AXFetchAsSregTransform.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider.Behaviors {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Behaviors;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.OpenId.Provider.Extensions;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// An Attribute Exchange and Simple Registration filter to make all incoming attribute
+ /// requests look like Simple Registration requests, and to convert the response
+ /// to the originally requested extension and format.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")]
+ public sealed class AXFetchAsSregTransform : AXFetchAsSregTransformBase, IProviderBehavior {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AXFetchAsSregTransform"/> class.
+ /// </summary>
+ public AXFetchAsSregTransform() {
+ }
+
+ #region IProviderBehavior Members
+
+ /// <summary>
+ /// Applies a well known set of security requirements to a default set of security settings.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) {
+ // Nothing to do here.
+ }
+
+ /// <summary>
+ /// Called when a request is received by the Provider.
+ /// </summary>
+ /// <param name="request">The incoming request.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ /// <remarks>
+ /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but
+ /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/>
+ /// itself as that instance may be shared across many requests.
+ /// </remarks>
+ bool IProviderBehavior.OnIncomingRequest(IRequest request) {
+ var extensionRequest = request as Provider.HostProcessedRequest;
+ if (extensionRequest != null) {
+ extensionRequest.UnifyExtensionsAsSreg();
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Called when the Provider is preparing to send a response to an authentication request.
+ /// </summary>
+ /// <param name="request">The request that is configured to generate the outgoing response.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ bool IProviderBehavior.OnOutgoingResponse(Provider.IAuthenticationRequest request) {
+ request.ConvertSregToMatchRequest();
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/GsaIcamProfile.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/GsaIcamProfile.cs
new file mode 100644
index 0000000..38f2ae7
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/GsaIcamProfile.cs
@@ -0,0 +1,193 @@
+//-----------------------------------------------------------------------
+// <copyright file="GsaIcamProfile.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider.Behaviors {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Behaviors;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Implements the Identity, Credential, &amp; Access Management (ICAM) OpenID 2.0 Profile
+ /// for the General Services Administration (GSA).
+ /// </summary>
+ /// <remarks>
+ /// <para>Relying parties that include this profile are always held to the terms required by the profile,
+ /// but Providers are only affected by the special behaviors of the profile when the RP specifically
+ /// indicates that they want to use this profile. </para>
+ /// </remarks>
+ [Serializable]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Icam", Justification = "Acronym")]
+ public sealed class GsaIcamProfile : GsaIcamProfileBase, IProviderBehavior {
+ /// <summary>
+ /// The maximum time a shared association can live.
+ /// </summary>
+ private static readonly TimeSpan MaximumAssociationLifetime = TimeSpan.FromSeconds(86400);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GsaIcamProfile"/> class.
+ /// </summary>
+ public GsaIcamProfile() {
+ if (DisableSslRequirement) {
+ Logger.OpenId.Warn("GSA level 1 behavior has its RequireSsl requirement disabled.");
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the provider for generating PPID identifiers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Acronym")]
+ public static IDirectedIdentityIdentifierProvider PpidIdentifierProvider { get; set; }
+
+ #region IProviderBehavior Members
+
+ /// <summary>
+ /// Adapts the default security settings to the requirements of this behavior.
+ /// </summary>
+ /// <param name="securitySettings">The original security settings.</param>
+ void IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) {
+ if (securitySettings.MaximumHashBitLength < 256) {
+ securitySettings.MaximumHashBitLength = 256;
+ }
+
+ SetMaximumAssociationLifetimeToNotExceed(Protocol.Default.Args.SignatureAlgorithm.HMAC_SHA256, MaximumAssociationLifetime, securitySettings);
+ SetMaximumAssociationLifetimeToNotExceed(Protocol.Default.Args.SignatureAlgorithm.HMAC_SHA1, MaximumAssociationLifetime, securitySettings);
+ }
+
+ /// <summary>
+ /// Called when a request is received by the Provider.
+ /// </summary>
+ /// <param name="request">The incoming request.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ /// <remarks>
+ /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but
+ /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/>
+ /// itself as that instance may be shared across many requests.
+ /// </remarks>
+ bool IProviderBehavior.OnIncomingRequest(IRequest request) {
+ var hostProcessedRequest = request as IHostProcessedRequest;
+ if (hostProcessedRequest != null) {
+ // Only apply our special policies if the RP requested it.
+ var papeRequest = request.GetExtension<PolicyRequest>();
+ if (papeRequest != null) {
+ if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) {
+ // Whenever we see this GSA policy requested, we MUST also see the PPID policy requested.
+ ErrorUtilities.VerifyProtocol(papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier), BehaviorStrings.PapeRequestMissingRequiredPolicies);
+ ErrorUtilities.VerifyProtocol(string.Equals(hostProcessedRequest.Realm.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal) || DisableSslRequirement, BehaviorStrings.RealmMustBeHttps);
+
+ // Apply GSA-specific security to this individual request.
+ request.SecuritySettings.RequireSsl = !DisableSslRequirement;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Called when the Provider is preparing to send a response to an authentication request.
+ /// </summary>
+ /// <param name="request">The request that is configured to generate the outgoing response.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ bool IProviderBehavior.OnOutgoingResponse(Provider.IAuthenticationRequest request) {
+ bool result = false;
+
+ // Nothing to do for negative assertions.
+ if (!request.IsAuthenticated.Value) {
+ return result;
+ }
+
+ var requestInternal = (Provider.AuthenticationRequest)request;
+ var responseMessage = (IProtocolMessageWithExtensions)requestInternal.Response;
+
+ // Only apply our special policies if the RP requested it.
+ var papeRequest = request.GetExtension<PolicyRequest>();
+ if (papeRequest != null) {
+ var papeResponse = responseMessage.Extensions.OfType<PolicyResponse>().SingleOrDefault();
+ if (papeResponse == null) {
+ request.AddResponseExtension(papeResponse = new PolicyResponse());
+ }
+
+ if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) {
+ result = true;
+ if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) {
+ papeResponse.ActualPolicies.Add(AuthenticationPolicies.USGovernmentTrustLevel1);
+ }
+
+ // The spec requires that the OP perform discovery and if that fails, it must either sternly
+ // warn the user of a potential threat or just abort the authentication.
+ // We can't verify that the OP displayed anything to the user at this level, but we can
+ // at least verify that the OP performed the discovery on the realm and halt things if it didn't.
+ ErrorUtilities.VerifyHost(requestInternal.HasRealmDiscoveryBeenPerformed, BehaviorStrings.RealmDiscoveryNotPerformed);
+ }
+
+ if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ ErrorUtilities.VerifyProtocol(request.ClaimedIdentifier == request.LocalIdentifier, OpenIdStrings.DelegatingIdentifiersNotAllowed);
+
+ // Mask the user's identity with a PPID.
+ ErrorUtilities.VerifyHost(PpidIdentifierProvider != null, BehaviorStrings.PpidProviderNotGiven);
+ Identifier ppidIdentifier = PpidIdentifierProvider.GetIdentifier(request.LocalIdentifier, request.Realm);
+ requestInternal.ResetClaimedAndLocalIdentifiers(ppidIdentifier);
+
+ // Indicate that the RP is receiving a PPID claimed_id
+ if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier);
+ }
+ }
+
+ if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) {
+ ErrorUtilities.VerifyProtocol(
+ !responseMessage.Extensions.OfType<ClaimsResponse>().Any() &&
+ !responseMessage.Extensions.OfType<FetchResponse>().Any(),
+ BehaviorStrings.PiiIncludedWithNoPiiPolicy);
+
+ // If no PII is given in extensions, and the claimed_id is a PPID, then we can state we issue no PII.
+ if (papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) {
+ papeResponse.ActualPolicies.Add(AuthenticationPolicies.NoPersonallyIdentifiableInformation);
+ }
+ }
+ }
+
+ Reporting.RecordEventOccurrence(this, "OP");
+ }
+
+ return result;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Ensures the maximum association lifetime does not exceed a given limit.
+ /// </summary>
+ /// <param name="associationType">Type of the association.</param>
+ /// <param name="maximumLifetime">The maximum lifetime.</param>
+ /// <param name="securitySettings">The security settings to adjust.</param>
+ private static void SetMaximumAssociationLifetimeToNotExceed(string associationType, TimeSpan maximumLifetime, ProviderSecuritySettings securitySettings) {
+ Contract.Requires(!String.IsNullOrEmpty(associationType));
+ Contract.Requires(maximumLifetime.TotalSeconds > 0);
+ if (!securitySettings.AssociationLifetimes.ContainsKey(associationType) ||
+ securitySettings.AssociationLifetimes[associationType] > maximumLifetime) {
+ securitySettings.AssociationLifetimes[associationType] = maximumLifetime;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/PpidGeneration.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/PpidGeneration.cs
new file mode 100644
index 0000000..1a6898e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Behaviors/PpidGeneration.cs
@@ -0,0 +1,122 @@
+//-----------------------------------------------------------------------
+// <copyright file="PpidGeneration.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider.Behaviors {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Behaviors;
+ using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy;
+ using DotNetOpenAuth.OpenId.Provider;
+
+ /// <summary>
+ /// Offers OpenID Providers automatic PPID Claimed Identifier generation when requested
+ /// by a PAPE request.
+ /// </summary>
+ /// <remarks>
+ /// <para>PPIDs are set on positive authentication responses when the PAPE request includes
+ /// the <see cref="AuthenticationPolicies.PrivatePersonalIdentifier"/> authentication policy.</para>
+ /// <para>The static member <see cref="PpidGeneration.PpidIdentifierProvider"/> MUST
+ /// be set prior to any PPID requests come in. Typically this should be set in the
+ /// <c>Application_Start</c> method in the global.asax.cs file.</para>
+ /// </remarks>
+ [Serializable]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Abbreviation")]
+ public sealed class PpidGeneration : IProviderBehavior {
+ /// <summary>
+ /// Gets or sets the provider for generating PPID identifiers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Abbreviation")]
+ public static IDirectedIdentityIdentifierProvider PpidIdentifierProvider { get; set; }
+
+ #region IProviderBehavior Members
+
+ /// <summary>
+ /// Applies a well known set of security requirements to a default set of security settings.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) {
+ // No special security to apply here.
+ }
+
+ /// <summary>
+ /// Called when a request is received by the Provider.
+ /// </summary>
+ /// <param name="request">The incoming request.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ /// <remarks>
+ /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but
+ /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/>
+ /// itself as that instance may be shared across many requests.
+ /// </remarks>
+ bool IProviderBehavior.OnIncomingRequest(IRequest request) {
+ return false;
+ }
+
+ /// <summary>
+ /// Called when the Provider is preparing to send a response to an authentication request.
+ /// </summary>
+ /// <param name="request">The request that is configured to generate the outgoing response.</param>
+ /// <returns>
+ /// <c>true</c> if this behavior owns this request and wants to stop other behaviors
+ /// from handling it; <c>false</c> to allow other behaviors to process this request.
+ /// </returns>
+ bool IProviderBehavior.OnOutgoingResponse(IAuthenticationRequest request) {
+ // Nothing to do for negative assertions.
+ if (!request.IsAuthenticated.Value) {
+ return false;
+ }
+
+ var requestInternal = (Provider.AuthenticationRequest)request;
+ var responseMessage = (IProtocolMessageWithExtensions)requestInternal.Response;
+
+ // Only apply our special policies if the RP requested it.
+ var papeRequest = request.GetExtension<PolicyRequest>();
+ if (papeRequest != null) {
+ if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ ErrorUtilities.VerifyProtocol(request.ClaimedIdentifier == request.LocalIdentifier, OpenIdStrings.DelegatingIdentifiersNotAllowed);
+
+ if (PpidIdentifierProvider == null) {
+ Logger.OpenId.Error(BehaviorStrings.PpidProviderNotGiven);
+ return false;
+ }
+
+ // Mask the user's identity with a PPID.
+ if (PpidIdentifierProvider.IsUserLocalIdentifier(request.LocalIdentifier)) {
+ Identifier ppidIdentifier = PpidIdentifierProvider.GetIdentifier(request.LocalIdentifier, request.Realm);
+ requestInternal.ResetClaimedAndLocalIdentifiers(ppidIdentifier);
+ }
+
+ // Indicate that the RP is receiving a PPID claimed_id
+ var papeResponse = responseMessage.Extensions.OfType<PolicyResponse>().SingleOrDefault();
+ if (papeResponse == null) {
+ request.AddResponseExtension(papeResponse = new PolicyResponse());
+ }
+
+ if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier);
+ }
+
+ Reporting.RecordEventOccurrence(this, string.Empty);
+ }
+ }
+
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/ExtensionsInteropHelper.cs
new file mode 100644
index 0000000..eda768b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/ExtensionsInteropHelper.cs
@@ -0,0 +1,162 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExtensionsInteropHelper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A set of methods designed to assist in improving interop across different
+ /// OpenID implementations and their extensions.
+ /// </summary>
+ internal static class ExtensionsInteropHelper {
+ /// <summary>
+ /// Transforms an AX attribute type URI from the axschema.org format into a given format.
+ /// </summary>
+ /// <param name="axSchemaOrgFormatTypeUri">The ax schema org format type URI.</param>
+ /// <param name="targetFormat">The target format. Only one flag should be set.</param>
+ /// <returns>The AX attribute type URI in the target format.</returns>
+ internal static string TransformAXFormatTestHook(string axSchemaOrgFormatTypeUri, AXAttributeFormats targetFormat) {
+ return OpenIdExtensionsInteropHelper.TransformAXFormat(axSchemaOrgFormatTypeUri, targetFormat);
+ }
+
+ /// <summary>
+ /// Looks for Simple Registration and Attribute Exchange (all known formats)
+ /// request extensions and returns them as a Simple Registration extension,
+ /// and adds the new extension to the original request message if it was absent.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <returns>
+ /// The Simple Registration request if found,
+ /// or a fabricated one based on the Attribute Exchange extension if found,
+ /// or <c>null</c> if no attribute extension request is found.</returns>
+ internal static ClaimsRequest UnifyExtensionsAsSreg(this Provider.IHostProcessedRequest request) {
+ Requires.NotNull(request, "request");
+
+ var req = (Provider.HostProcessedRequest)request;
+ var sreg = req.GetExtension<ClaimsRequest>();
+ if (sreg != null) {
+ return sreg;
+ }
+
+ var ax = req.GetExtension<FetchRequest>();
+ if (ax != null) {
+ sreg = new ClaimsRequest(DotNetOpenAuth.OpenId.Extensions.SimpleRegistration.Constants.sreg_ns);
+ sreg.Synthesized = true;
+ ((IProtocolMessageWithExtensions)req.RequestMessage).Extensions.Add(sreg);
+ sreg.BirthDate = GetDemandLevelFor(ax, WellKnownAttributes.BirthDate.WholeBirthDate);
+ sreg.Country = GetDemandLevelFor(ax, WellKnownAttributes.Contact.HomeAddress.Country);
+ sreg.Email = GetDemandLevelFor(ax, WellKnownAttributes.Contact.Email);
+ sreg.FullName = GetDemandLevelFor(ax, WellKnownAttributes.Name.FullName);
+ sreg.Gender = GetDemandLevelFor(ax, WellKnownAttributes.Person.Gender);
+ sreg.Language = GetDemandLevelFor(ax, WellKnownAttributes.Preferences.Language);
+ sreg.Nickname = GetDemandLevelFor(ax, WellKnownAttributes.Name.Alias);
+ sreg.PostalCode = GetDemandLevelFor(ax, WellKnownAttributes.Contact.HomeAddress.PostalCode);
+ sreg.TimeZone = GetDemandLevelFor(ax, WellKnownAttributes.Preferences.TimeZone);
+ }
+
+ return sreg;
+ }
+
+ /// <summary>
+ /// Converts the Simple Registration extension response to whatever format the original
+ /// attribute request extension came in.
+ /// </summary>
+ /// <param name="request">The authentication request with the response extensions already added.</param>
+ /// <remarks>
+ /// If the original attribute request came in as AX, the Simple Registration extension is converted
+ /// to an AX response and then the Simple Registration extension is removed from the response.
+ /// </remarks>
+ internal static void ConvertSregToMatchRequest(this Provider.IHostProcessedRequest request) {
+ var req = (Provider.HostProcessedRequest)request;
+ var response = req.Response as IProtocolMessageWithExtensions; // negative responses don't support extensions.
+ var sregRequest = request.GetExtension<ClaimsRequest>();
+ if (sregRequest != null && response != null) {
+ if (sregRequest.Synthesized) {
+ var axRequest = request.GetExtension<FetchRequest>();
+ ErrorUtilities.VerifyInternal(axRequest != null, "How do we have a synthesized Sreg request without an AX request?");
+
+ var sregResponse = response.Extensions.OfType<ClaimsResponse>().SingleOrDefault();
+ if (sregResponse == null) {
+ // No Sreg response to copy from.
+ return;
+ }
+
+ // Remove the sreg response since the RP didn't ask for it.
+ response.Extensions.Remove(sregResponse);
+
+ AXAttributeFormats format = OpenIdExtensionsInteropHelper.DetectAXFormat(axRequest.Attributes.Select(att => att.TypeUri));
+ if (format == AXAttributeFormats.None) {
+ // No recognized AX attributes were requested.
+ return;
+ }
+
+ var axResponse = response.Extensions.OfType<FetchResponse>().SingleOrDefault();
+ if (axResponse == null) {
+ axResponse = new FetchResponse();
+ response.Extensions.Add(axResponse);
+ }
+
+ AddAXAttributeValue(axResponse, WellKnownAttributes.BirthDate.WholeBirthDate, format, sregResponse.BirthDateRaw);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.HomeAddress.Country, format, sregResponse.Country);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.HomeAddress.PostalCode, format, sregResponse.PostalCode);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.Email, format, sregResponse.Email);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Name.FullName, format, sregResponse.FullName);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Name.Alias, format, sregResponse.Nickname);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Preferences.TimeZone, format, sregResponse.TimeZone);
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Preferences.Language, format, sregResponse.Language);
+ if (sregResponse.Gender.HasValue) {
+ AddAXAttributeValue(axResponse, WellKnownAttributes.Person.Gender, format, OpenIdExtensionsInteropHelper.GenderEncoder.Encode(sregResponse.Gender));
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Adds the AX attribute value to the response if it is non-empty.
+ /// </summary>
+ /// <param name="ax">The AX Fetch response to add the attribute value to.</param>
+ /// <param name="typeUri">The attribute type URI in axschema.org format.</param>
+ /// <param name="format">The target format of the actual attribute to write out.</param>
+ /// <param name="value">The value of the attribute.</param>
+ private static void AddAXAttributeValue(FetchResponse ax, string typeUri, AXAttributeFormats format, string value) {
+ if (!string.IsNullOrEmpty(value)) {
+ string targetTypeUri = OpenIdExtensionsInteropHelper.TransformAXFormat(typeUri, format);
+ if (!ax.Attributes.Contains(targetTypeUri)) {
+ ax.Attributes.Add(targetTypeUri, value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the demand level for an AX attribute.
+ /// </summary>
+ /// <param name="ax">The AX fetch request to search for the attribute.</param>
+ /// <param name="typeUri">The type URI of the attribute in axschema.org format.</param>
+ /// <returns>The demand level for the attribute.</returns>
+ private static DemandLevel GetDemandLevelFor(FetchRequest ax, string typeUri) {
+ Requires.NotNull(ax, "ax");
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+
+ foreach (AXAttributeFormats format in OpenIdExtensionsInteropHelper.ForEachFormat(AXAttributeFormats.All)) {
+ string typeUriInFormat = OpenIdExtensionsInteropHelper.TransformAXFormat(typeUri, format);
+ if (ax.Attributes.Contains(typeUriInFormat)) {
+ return ax.Attributes[typeUriInFormat].IsRequired ? DemandLevel.Require : DemandLevel.Request;
+ }
+ }
+
+ return DemandLevel.NoRequest;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/UI/UIRequestTools.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/UI/UIRequestTools.cs
new file mode 100644
index 0000000..80ee2f1
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Extensions/UI/UIRequestTools.cs
@@ -0,0 +1,68 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIRequestTools.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider.Extensions.UI {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.Xrds;
+
+ /// <summary>
+ /// OpenID User Interface extension 1.0 request message.
+ /// </summary>
+ /// <remarks>
+ /// <para>Implements the extension described by: http://wiki.openid.net/f/openid_ui_extension_draft01.html </para>
+ /// <para>This extension only applies to checkid_setup requests, since checkid_immediate requests display
+ /// no UI to the user. </para>
+ /// <para>For rules about how the popup window should be displayed, please see the documentation of
+ /// <see cref="UIModes.Popup"/>. </para>
+ /// <para>An RP may determine whether an arbitrary OP supports this extension (and thereby determine
+ /// whether to use a standard full window redirect or a popup) via the
+ /// <see cref="IdentifierDiscoveryResult.IsExtensionSupported&lt;T&gt;()"/> method.</para>
+ /// </remarks>
+ public static class UIRequestTools {
+ /// <summary>
+ /// Gets the URL of the RP icon for the OP to display.
+ /// </summary>
+ /// <param name="realm">The realm of the RP where the authentication request originated.</param>
+ /// <param name="webRequestHandler">The web request handler to use for discovery.
+ /// Usually available via <see cref="Channel.WebRequestHandler">OpenIdProvider.Channel.WebRequestHandler</see>.</param>
+ /// <returns>
+ /// A sequence of the RP's icons it has available for the Provider to display, in decreasing preferred order.
+ /// </returns>
+ /// <value>The icon URL.</value>
+ /// <remarks>
+ /// This property is automatically set for the OP with the result of RP discovery.
+ /// RPs should set this value by including an entry such as this in their XRDS document.
+ /// <example>
+ /// &lt;Service xmlns="xri://$xrd*($v*2.0)"&gt;
+ /// &lt;Type&gt;http://specs.openid.net/extensions/ui/icon&lt;/Type&gt;
+ /// &lt;URI&gt;http://consumer.example.com/images/image.jpg&lt;/URI&gt;
+ /// &lt;/Service&gt;
+ /// </example>
+ /// </remarks>
+ public static IEnumerable<Uri> GetRelyingPartyIconUrls(Realm realm, IDirectWebRequestHandler webRequestHandler) {
+ Contract.Requires(realm != null);
+ Contract.Requires(webRequestHandler != null);
+ ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
+ ErrorUtilities.VerifyArgumentNotNull(webRequestHandler, "webRequestHandler");
+
+ XrdsDocument xrds = realm.Discover(webRequestHandler, false);
+ if (xrds == null) {
+ return Enumerable.Empty<Uri>();
+ } else {
+ return xrds.FindRelyingPartyIcons();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HmacShaAssociationProvider.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HmacShaAssociationProvider.cs
new file mode 100644
index 0000000..3cfc0b6
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HmacShaAssociationProvider.cs
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------
+// <copyright file="HmacShaAssociationProvider.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Provider;
+
+ /// <summary>
+ /// OpenID Provider utility methods for HMAC-SHA* associations.
+ /// </summary>
+ internal static class HmacShaAssociationProvider {
+ /// <summary>
+ /// The default lifetime of a shared association when no lifetime is given
+ /// for a specific association type.
+ /// </summary>
+ private static readonly TimeSpan DefaultMaximumLifetime = TimeSpan.FromDays(14);
+
+ /// <summary>
+ /// Creates a new association of a given type at an OpenID Provider.
+ /// </summary>
+ /// <param name="protocol">The protocol.</param>
+ /// <param name="associationType">Type of the association (i.e. HMAC-SHA1 or HMAC-SHA256)</param>
+ /// <param name="associationUse">A value indicating whether the new association will be used privately by the Provider for "dumb mode" authentication
+ /// or shared with the Relying Party for "smart mode" authentication.</param>
+ /// <param name="associationStore">The Provider's association store.</param>
+ /// <param name="securitySettings">The security settings of the Provider.</param>
+ /// <returns>
+ /// The newly created association.
+ /// </returns>
+ /// <remarks>
+ /// The new association is NOT automatically put into an association store. This must be done by the caller.
+ /// </remarks>
+ internal static HmacShaAssociation Create(Protocol protocol, string associationType, AssociationRelyingPartyType associationUse, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(protocol, "protocol");
+ Requires.NotNullOrEmpty(associationType, "associationType");
+ Requires.NotNull(associationStore, "associationStore");
+ Requires.NotNull(securitySettings, "securitySettings");
+ Contract.Ensures(Contract.Result<HmacShaAssociation>() != null);
+
+ int secretLength = HmacShaAssociation.GetSecretLength(protocol, associationType);
+
+ // Generate the secret that will be used for signing
+ byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength);
+
+ TimeSpan lifetime;
+ if (associationUse == AssociationRelyingPartyType.Smart) {
+ if (!securitySettings.AssociationLifetimes.TryGetValue(associationType, out lifetime)) {
+ lifetime = DefaultMaximumLifetime;
+ }
+ } else {
+ lifetime = HmacShaAssociation.DumbSecretLifetime;
+ }
+
+ string handle = associationStore.Serialize(secret, DateTime.UtcNow + lifetime, associationUse == AssociationRelyingPartyType.Dumb);
+
+ Contract.Assert(protocol != null); // All the way up to the method call, the condition holds, yet we get a Requires failure next
+ Contract.Assert(secret != null);
+ Contract.Assert(!String.IsNullOrEmpty(associationType));
+ var result = HmacShaAssociation.Create(protocol, associationType, handle, secret, lifetime);
+ return result;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HostProcessedRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HostProcessedRequest.cs
new file mode 100644
index 0000000..3647a63
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/HostProcessedRequest.cs
@@ -0,0 +1,182 @@
+//-----------------------------------------------------------------------
+// <copyright file="HostProcessedRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A base class from which identity and non-identity RP requests can derive.
+ /// </summary>
+ [Serializable]
+ internal abstract class HostProcessedRequest : Request, IHostProcessedRequest {
+ /// <summary>
+ /// The negative assertion to send, if the host site chooses to send it.
+ /// </summary>
+ private readonly NegativeAssertionResponse negativeResponse;
+
+ /// <summary>
+ /// A cache of the result from discovery of the Realm URL.
+ /// </summary>
+ private RelyingPartyDiscoveryResult? realmDiscoveryResult;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostProcessedRequest"/> class.
+ /// </summary>
+ /// <param name="provider">The provider that received the request.</param>
+ /// <param name="request">The incoming request message.</param>
+ protected HostProcessedRequest(OpenIdProvider provider, SignedResponseRequest request)
+ : base(request, provider.SecuritySettings) {
+ Requires.NotNull(provider, "provider");
+
+ this.negativeResponse = new NegativeAssertionResponse(request, provider.Channel);
+ Reporting.RecordEventOccurrence(this, request.Realm);
+ }
+
+ #region IHostProcessedRequest Properties
+
+ /// <summary>
+ /// Gets the version of OpenID being used by the relying party that sent the request.
+ /// </summary>
+ public ProtocolVersion RelyingPartyVersion {
+ get { return Protocol.Lookup(this.RequestMessage.Version).ProtocolVersion; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the consumer demands an immediate response.
+ /// If false, the consumer is willing to wait for the identity provider
+ /// to authenticate the user.
+ /// </summary>
+ public bool Immediate {
+ get { return this.RequestMessage.Immediate; }
+ }
+
+ /// <summary>
+ /// Gets the URL the consumer site claims to use as its 'base' address.
+ /// </summary>
+ public Realm Realm {
+ get { return this.RequestMessage.Realm; }
+ }
+
+ /// <summary>
+ /// Gets or sets the provider endpoint.
+ /// </summary>
+ /// <value>
+ /// The default value is the URL that the request came in on from the relying party.
+ /// </value>
+ public abstract Uri ProviderEndpoint { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets a value indicating whether realm discovery been performed.
+ /// </summary>
+ internal bool HasRealmDiscoveryBeenPerformed {
+ get { return this.realmDiscoveryResult.HasValue; }
+ }
+
+ /// <summary>
+ /// Gets the negative response.
+ /// </summary>
+ protected NegativeAssertionResponse NegativeResponse {
+ get { return this.negativeResponse; }
+ }
+
+ /// <summary>
+ /// Gets the original request message.
+ /// </summary>
+ /// <value>This may be null in the case of an unrecognizable message.</value>
+ protected new SignedResponseRequest RequestMessage {
+ get { return (SignedResponseRequest)base.RequestMessage; }
+ }
+
+ #region IHostProcessedRequest Methods
+
+ /// <summary>
+ /// Gets a value indicating whether verification of the return URL claimed by the Relying Party
+ /// succeeded.
+ /// </summary>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <returns>
+ /// Result of realm discovery.
+ /// </returns>
+ /// <remarks>
+ /// Return URL verification is only attempted if this property is queried.
+ /// The result of the verification is cached per request so calling this
+ /// property getter multiple times in one request is not a performance hit.
+ /// See OpenID Authentication 2.0 spec section 9.2.1.
+ /// </remarks>
+ public RelyingPartyDiscoveryResult IsReturnUrlDiscoverable(IDirectWebRequestHandler requestHandler) {
+ if (!this.realmDiscoveryResult.HasValue) {
+ this.realmDiscoveryResult = this.IsReturnUrlDiscoverableCore(requestHandler);
+ }
+
+ return this.realmDiscoveryResult.Value;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether verification of the return URL claimed by the Relying Party
+ /// succeeded.
+ /// </summary>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <returns>
+ /// Result of realm discovery.
+ /// </returns>
+ private RelyingPartyDiscoveryResult IsReturnUrlDiscoverableCore(IDirectWebRequestHandler requestHandler) {
+ Requires.NotNull(requestHandler, "requestHandler");
+
+ ErrorUtilities.VerifyInternal(this.Realm != null, "Realm should have been read or derived by now.");
+
+ try {
+ if (this.SecuritySettings.RequireSsl && this.Realm.Scheme != Uri.UriSchemeHttps) {
+ Logger.OpenId.WarnFormat("RP discovery failed because RequireSsl is true and RP discovery would begin at insecure URL {0}.", this.Realm);
+ return RelyingPartyDiscoveryResult.NoServiceDocument;
+ }
+
+ var returnToEndpoints = this.Realm.DiscoverReturnToEndpoints(requestHandler, false);
+ if (returnToEndpoints == null) {
+ return RelyingPartyDiscoveryResult.NoServiceDocument;
+ }
+
+ foreach (var returnUrl in returnToEndpoints) {
+ Realm discoveredReturnToUrl = returnUrl.ReturnToEndpoint;
+
+ // The spec requires that the return_to URLs given in an RPs XRDS doc
+ // do not contain wildcards.
+ if (discoveredReturnToUrl.DomainWildcard) {
+ Logger.Yadis.WarnFormat("Realm {0} contained return_to URL {1} which contains a wildcard, which is not allowed.", Realm, discoveredReturnToUrl);
+ continue;
+ }
+
+ // Use the same rules as return_to/realm matching to check whether this
+ // URL fits the return_to URL we were given.
+ if (discoveredReturnToUrl.Contains(this.RequestMessage.ReturnTo)) {
+ // no need to keep looking after we find a match
+ return RelyingPartyDiscoveryResult.Success;
+ }
+ }
+ } catch (ProtocolException ex) {
+ // Don't do anything else. We quietly fail at return_to verification and return false.
+ Logger.Yadis.InfoFormat("Relying party discovery at URL {0} failed. {1}", Realm, ex);
+ return RelyingPartyDiscoveryResult.NoServiceDocument;
+ } catch (WebException ex) {
+ // Don't do anything else. We quietly fail at return_to verification and return false.
+ Logger.Yadis.InfoFormat("Relying party discovery at URL {0} failed. {1}", Realm, ex);
+ return RelyingPartyDiscoveryResult.NoServiceDocument;
+ }
+
+ return RelyingPartyDiscoveryResult.NoMatchingReturnTo;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IAnonymousRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IAnonymousRequest.cs
new file mode 100644
index 0000000..ec2c175
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IAnonymousRequest.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------
+// <copyright file="IAnonymousRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// Instances of this interface represent incoming extension-only requests.
+ /// This interface provides the details of the request and allows setting
+ /// the response.
+ /// </summary>
+ public interface IAnonymousRequest : IHostProcessedRequest {
+ /// <summary>
+ /// Gets or sets a value indicating whether the user approved sending any data to the relying party.
+ /// </summary>
+ /// <value><c>true</c> if approved; otherwise, <c>false</c>.</value>
+ bool? IsApproved { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs
new file mode 100644
index 0000000..9197761
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs
@@ -0,0 +1,83 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectedIdentityIdentifierProvider.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// An interface to provide custom identifiers for users logging into specific relying parties.
+ /// </summary>
+ /// <remarks>
+ /// This interface would allow, for example, the Provider to offer PPIDs to their users,
+ /// allowing the users to log into RPs without leaving any clue as to their true identity,
+ /// and preventing multiple RPs from colluding to track user activity across realms.
+ /// </remarks>
+ [ContractClass(typeof(IDirectedIdentityIdentifierProviderContract))]
+ public interface IDirectedIdentityIdentifierProvider {
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm);
+
+ /// <summary>
+ /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user.
+ /// </summary>
+ /// <param name="identifier">The identifier in question.</param>
+ /// <returns>
+ /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ bool IsUserLocalIdentifier(Identifier identifier);
+ }
+
+ /// <summary>
+ /// Contract class for the <see cref="IDirectedIdentityIdentifierProvider"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IDirectedIdentityIdentifierProvider))]
+ internal abstract class IDirectedIdentityIdentifierProviderContract : IDirectedIdentityIdentifierProvider {
+ #region IDirectedIdentityIdentifierProvider Members
+
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ Uri IDirectedIdentityIdentifierProvider.GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
+ Requires.NotNull(localIdentifier, "localIdentifier");
+ Requires.NotNull(relyingPartyRealm, "relyingPartyRealm");
+ Requires.True(((IDirectedIdentityIdentifierProvider)this).IsUserLocalIdentifier(localIdentifier), "localIdentifier", OpenIdStrings.ArgumentIsPpidIdentifier);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user.
+ /// </summary>
+ /// <param name="identifier">The identifier in question.</param>
+ /// <returns>
+ /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>.
+ /// </returns>
+ bool IDirectedIdentityIdentifierProvider.IsUserLocalIdentifier(Identifier identifier) {
+ Requires.NotNull(identifier, "identifier");
+
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IErrorReporting.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IErrorReporting.cs
new file mode 100644
index 0000000..1c73595
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IErrorReporting.cs
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------
+// <copyright file="IErrorReporting.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An interface that a Provider site may implement in order to better
+ /// control error reporting.
+ /// </summary>
+ public interface IErrorReporting {
+ /// <summary>
+ /// Gets the message that can be sent in an error response
+ /// with information on who the remote party can contact
+ /// for help resolving the error.
+ /// </summary>
+ /// <value>
+ /// The contact address may take any form, as it is intended to be displayed to a person.
+ /// </value>
+ string Contact { get; }
+
+ /// <summary>
+ /// Logs the details of an exception for later reference in diagnosing the problem.
+ /// </summary>
+ /// <param name="exception">The exception that was generated from the error.</param>
+ /// <returns>
+ /// A unique identifier for this particular error that the remote party can
+ /// reference when contacting <see cref="Contact"/> for help with this error.
+ /// May be null.
+ /// </returns>
+ /// <remarks>
+ /// The implementation of this method should never throw an unhandled exception
+ /// as that would preclude the ability to send the error response to the remote
+ /// party. When this method is not implemented, it should return null rather
+ /// than throwing <see cref="NotImplementedException"/>.
+ /// </remarks>
+ string LogError(ProtocolException exception);
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IProviderAssociationStore.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IProviderAssociationStore.cs
new file mode 100644
index 0000000..6c749f6
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/IProviderAssociationStore.cs
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------
+// <copyright file="IProviderAssociationStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Provides association serialization and deserialization.
+ /// </summary>
+ /// <remarks>
+ /// Implementations may choose to store the association details in memory or a database table and simply return a
+ /// short, randomly generated string that is the key to that data. Alternatively, an implementation may
+ /// sign and encrypt the association details and then encode the results as a base64 string and return that value
+ /// as the association handle, thereby avoiding any association persistence at the OpenID Provider.
+ /// When taking the latter approach however, it is of course imperative that the association be encrypted
+ /// to avoid disclosing the secret to anyone who sees the association handle, which itself isn't considered to
+ /// be confidential.
+ /// </remarks>
+ [ContractClass(typeof(IProviderAssociationStoreContract))]
+ internal interface IProviderAssociationStore {
+ /// <summary>
+ /// Stores an association and returns a handle for it.
+ /// </summary>
+ /// <param name="secret">The association secret.</param>
+ /// <param name="expiresUtc">The UTC time that the association should expire.</param>
+ /// <param name="privateAssociation">A value indicating whether this is a private association.</param>
+ /// <returns>
+ /// The association handle that represents this association.
+ /// </returns>
+ string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation);
+
+ /// <summary>
+ /// Retrieves an association given an association handle.
+ /// </summary>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="privateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed).
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
+ Association Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IProviderAssociationStore"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IProviderAssociationStore))]
+ internal abstract class IProviderAssociationStoreContract : IProviderAssociationStore {
+ /// <summary>
+ /// Stores an association and returns a handle for it.
+ /// </summary>
+ /// <param name="secret">The association secret.</param>
+ /// <param name="expiresUtc">The expires UTC.</param>
+ /// <param name="privateAssociation">A value indicating whether this is a private association.</param>
+ /// <returns>
+ /// The association handle that represents this association.
+ /// </returns>
+ string IProviderAssociationStore.Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ Requires.NotNull(secret, "secret");
+ Requires.True(expiresUtc.Kind == DateTimeKind.Utc, "expiresUtc");
+ Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Retrieves an association given an association handle.
+ /// </summary>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="privateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed).
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
+ Association IProviderAssociationStore.Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle) {
+ Requires.NotNull(containingMessage, "containingMessage");
+ Requires.NotNullOrEmpty(handle, "handle");
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProvider.cs
new file mode 100644
index 0000000..6b78098
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProvider.cs
@@ -0,0 +1,674 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdProvider.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Threading;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+ using RP = DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Offers services for a web page that is acting as an OpenID identity server.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")]
+ [ContractVerification(true)]
+ public sealed class OpenIdProvider : IDisposable {
+ /// <summary>
+ /// The name of the key to use in the HttpApplication cache to store the
+ /// instance of <see cref="StandardProviderApplicationStore"/> to use.
+ /// </summary>
+ private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.Provider.OpenIdProvider.ApplicationStore";
+
+ /// <summary>
+ /// Backing store for the <see cref="Behaviors"/> property.
+ /// </summary>
+ private readonly ObservableCollection<IProviderBehavior> behaviors = new ObservableCollection<IProviderBehavior>();
+
+ /// <summary>
+ /// A type initializer that ensures that another type initializer runs in order to guarantee that
+ /// types are serializable.
+ /// </summary>
+ private static Identifier dummyIdentifierToInvokeStaticCtor = "http://localhost/";
+
+ /// <summary>
+ /// A type initializer that ensures that another type initializer runs in order to guarantee that
+ /// types are serializable.
+ /// </summary>
+ private static Realm dummyRealmToInvokeStaticCtor = "http://localhost/";
+
+ /// <summary>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private ProviderSecuritySettings securitySettings;
+
+ /// <summary>
+ /// The relying party used to perform discovery on identifiers being sent in
+ /// unsolicited positive assertions.
+ /// </summary>
+ private RP.OpenIdRelyingParty relyingParty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdProvider"/> class.
+ /// </summary>
+ public OpenIdProvider()
+ : this(OpenIdElement.Configuration.Provider.ApplicationStore.CreateInstance(HttpApplicationStore)) {
+ Contract.Ensures(this.SecuritySettings != null);
+ Contract.Ensures(this.Channel != null);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdProvider"/> class.
+ /// </summary>
+ /// <param name="applicationStore">The application store to use. Cannot be null.</param>
+ public OpenIdProvider(IOpenIdApplicationStore applicationStore)
+ : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore) {
+ Requires.NotNull(applicationStore, "applicationStore");
+ Contract.Ensures(this.SecuritySettings != null);
+ Contract.Ensures(this.Channel != null);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdProvider"/> class.
+ /// </summary>
+ /// <param name="nonceStore">The nonce store to use. Cannot be null.</param>
+ /// <param name="cryptoKeyStore">The crypto key store. Cannot be null.</param>
+ private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) {
+ Requires.NotNull(nonceStore, "nonceStore");
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Contract.Ensures(this.SecuritySettings != null);
+ Contract.Ensures(this.Channel != null);
+
+ this.SecuritySettings = OpenIdElement.Configuration.Provider.SecuritySettings.CreateSecuritySettings();
+ this.behaviors.CollectionChanged += this.OnBehaviorsChanged;
+ foreach (var behavior in OpenIdElement.Configuration.Provider.Behaviors.CreateInstances(false)) {
+ this.behaviors.Add(behavior);
+ }
+
+ this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings);
+ this.Channel = new OpenIdProviderChannel(this.AssociationStore, nonceStore, this.SecuritySettings);
+ this.CryptoKeyStore = cryptoKeyStore;
+
+ Reporting.RecordFeatureAndDependencyUse(this, nonceStore);
+ }
+
+ /// <summary>
+ /// Gets the standard state storage mechanism that uses ASP.NET's
+ /// HttpApplication state dictionary to store associations and nonces.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IOpenIdApplicationStore HttpApplicationStore {
+ get {
+ Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null);
+ HttpContext context = HttpContext.Current;
+ var store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey];
+ if (store == null) {
+ context.Application.Lock();
+ try {
+ if ((store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey]) == null) {
+ context.Application[ApplicationStoreKey] = store = new StandardProviderApplicationStore();
+ }
+ } finally {
+ context.Application.UnLock();
+ }
+ }
+
+ return store;
+ }
+ }
+
+ /// <summary>
+ /// Gets the channel to use for sending/receiving messages.
+ /// </summary>
+ public Channel Channel { get; internal set; }
+
+ /// <summary>
+ /// Gets the security settings used by this Provider.
+ /// </summary>
+ public ProviderSecuritySettings SecuritySettings {
+ get {
+ Contract.Ensures(Contract.Result<ProviderSecuritySettings>() != null);
+ Contract.Assume(this.securitySettings != null);
+ return this.securitySettings;
+ }
+
+ internal set {
+ Requires.NotNull(value, "value");
+ this.securitySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the extension factories.
+ /// </summary>
+ public IList<IOpenIdExtensionFactory> ExtensionFactories {
+ get { return this.Channel.GetExtensionFactories(); }
+ }
+
+ /// <summary>
+ /// Gets or sets the mechanism a host site can use to receive
+ /// notifications of errors when communicating with remote parties.
+ /// </summary>
+ public IErrorReporting ErrorReporting { get; set; }
+
+ /// <summary>
+ /// Gets a list of custom behaviors to apply to OpenID actions.
+ /// </summary>
+ /// <remarks>
+ /// Adding behaviors can impact the security settings of the <see cref="OpenIdProvider"/>
+ /// in ways that subsequently removing the behaviors will not reverse.
+ /// </remarks>
+ public ICollection<IProviderBehavior> Behaviors {
+ get { return this.behaviors; }
+ }
+
+ /// <summary>
+ /// Gets the crypto key store.
+ /// </summary>
+ public ICryptoKeyStore CryptoKeyStore { get; private set; }
+
+ /// <summary>
+ /// Gets the association store.
+ /// </summary>
+ internal IProviderAssociationStore AssociationStore { get; private set; }
+
+ /// <summary>
+ /// Gets the channel.
+ /// </summary>
+ internal OpenIdChannel OpenIdChannel {
+ get { return (OpenIdChannel)this.Channel; }
+ }
+
+ /// <summary>
+ /// Gets the list of services that can perform discovery on identifiers given to this relying party.
+ /// </summary>
+ internal IList<IIdentifierDiscoveryService> DiscoveryServices {
+ get { return this.RelyingParty.DiscoveryServices; }
+ }
+
+ /// <summary>
+ /// Gets the web request handler to use for discovery and the part of
+ /// authentication where direct messages are sent to an untrusted remote party.
+ /// </summary>
+ internal IDirectWebRequestHandler WebRequestHandler {
+ get { return this.Channel.WebRequestHandler; }
+ }
+
+ /// <summary>
+ /// Gets the relying party used for discovery of identifiers sent in unsolicited assertions.
+ /// </summary>
+ private RP.OpenIdRelyingParty RelyingParty {
+ get {
+ if (this.relyingParty == null) {
+ lock (this) {
+ if (this.relyingParty == null) {
+ // we just need an RP that's capable of discovery, so stateless mode is fine.
+ this.relyingParty = new RP.OpenIdRelyingParty(null);
+ }
+ }
+ }
+
+ this.relyingParty.Channel.WebRequestHandler = this.WebRequestHandler;
+ return this.relyingParty;
+ }
+ }
+
+ /// <summary>
+ /// Gets the incoming OpenID request if there is one, or null if none was detected.
+ /// </summary>
+ /// <returns>The request that the hosting Provider should possibly process and then transmit the response for.</returns>
+ /// <remarks>
+ /// <para>Requests may be infrastructural to OpenID and allow auto-responses, or they may
+ /// be authentication requests where the Provider site has to make decisions based
+ /// on its own user database and policies.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ /// <exception cref="ProtocolException">Thrown if the incoming message is recognized but deviates from the protocol specification irrecoverably.</exception>
+ public IRequest GetRequest() {
+ return this.GetRequest(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets the incoming OpenID request if there is one, or null if none was detected.
+ /// </summary>
+ /// <param name="httpRequestInfo">The incoming HTTP request to extract the message from.</param>
+ /// <returns>
+ /// The request that the hosting Provider should process and then transmit the response for.
+ /// Null if no valid OpenID request was detected in the given HTTP request.
+ /// </returns>
+ /// <remarks>
+ /// Requests may be infrastructural to OpenID and allow auto-responses, or they may
+ /// be authentication requests where the Provider site has to make decisions based
+ /// on its own user database and policies.
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the incoming message is recognized
+ /// but deviates from the protocol specification irrecoverably.</exception>
+ public IRequest GetRequest(HttpRequestInfo httpRequestInfo) {
+ Requires.NotNull(httpRequestInfo, "httpRequestInfo");
+ IDirectedProtocolMessage incomingMessage = null;
+
+ try {
+ incomingMessage = this.Channel.ReadFromRequest(httpRequestInfo);
+ if (incomingMessage == null) {
+ // If the incoming request does not resemble an OpenID message at all,
+ // it's probably a user who just navigated to this URL, and we should
+ // just return null so the host can display a message to the user.
+ if (httpRequestInfo.HttpMethod == "GET" && !httpRequestInfo.UrlBeforeRewriting.QueryStringContainPrefixedParameters(Protocol.Default.openid.Prefix)) {
+ return null;
+ }
+
+ ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ }
+
+ IRequest result = null;
+
+ var checkIdMessage = incomingMessage as CheckIdRequest;
+ if (checkIdMessage != null) {
+ result = new AuthenticationRequest(this, checkIdMessage);
+ }
+
+ if (result == null) {
+ var extensionOnlyRequest = incomingMessage as SignedResponseRequest;
+ if (extensionOnlyRequest != null) {
+ result = new AnonymousRequest(this, extensionOnlyRequest);
+ }
+ }
+
+ if (result == null) {
+ var checkAuthMessage = incomingMessage as CheckAuthenticationRequest;
+ if (checkAuthMessage != null) {
+ result = new AutoResponsiveRequest(incomingMessage, new CheckAuthenticationResponseProvider(checkAuthMessage, this), this.SecuritySettings);
+ }
+ }
+
+ if (result == null) {
+ var associateMessage = incomingMessage as IAssociateRequestProvider;
+ if (associateMessage != null) {
+ result = new AutoResponsiveRequest(incomingMessage, AssociateRequestProviderTools.CreateResponse(associateMessage, this.AssociationStore, this.SecuritySettings), this.SecuritySettings);
+ }
+ }
+
+ if (result != null) {
+ foreach (var behavior in this.Behaviors) {
+ if (behavior.OnIncomingRequest(result)) {
+ // This behavior matched this request.
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ } catch (ProtocolException ex) {
+ IRequest errorResponse = this.GetErrorResponse(ex, httpRequestInfo, incomingMessage);
+ if (errorResponse == null) {
+ throw;
+ }
+
+ return errorResponse;
+ }
+ }
+
+ /// <summary>
+ /// Sends the response to a received request.
+ /// </summary>
+ /// <param name="request">The incoming OpenID request whose response is to be sent.</param>
+ /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception>
+ /// <remarks>
+ /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use
+ /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/>
+ /// to the client.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception>
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void SendResponse(IRequest request) {
+ Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
+ Requires.NotNull(request, "request");
+ Requires.True(request.IsResponseReady, "request");
+
+ this.ApplyBehaviorsToResponse(request);
+ Request requestInternal = (Request)request;
+ this.Channel.Send(requestInternal.Response);
+ }
+
+ /// <summary>
+ /// Sends the response to a received request.
+ /// </summary>
+ /// <param name="request">The incoming OpenID request whose response is to be sent.</param>
+ /// <remarks>
+ /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use
+ /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/>
+ /// to the client.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception>
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")]
+ public void Respond(IRequest request) {
+ Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
+ Requires.NotNull(request, "request");
+ Requires.True(request.IsResponseReady, "request");
+
+ this.ApplyBehaviorsToResponse(request);
+ Request requestInternal = (Request)request;
+ this.Channel.Respond(requestInternal.Response);
+ }
+
+ /// <summary>
+ /// Gets the response to a received request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>The response that should be sent to the client.</returns>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception>
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")]
+ public OutgoingWebResponse PrepareResponse(IRequest request) {
+ Requires.NotNull(request, "request");
+ Requires.True(request.IsResponseReady, "request");
+
+ this.ApplyBehaviorsToResponse(request);
+ Request requestInternal = (Request)request;
+ return this.Channel.PrepareResponse(requestInternal.Response);
+ }
+
+ /// <summary>
+ /// Sends an identity assertion on behalf of one of this Provider's
+ /// members in order to redirect the user agent to a relying party
+ /// web site and log him/her in immediately in one uninterrupted step.
+ /// </summary>
+ /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param>
+ /// <param name="relyingPartyRealm">The URL of the Relying Party web site.
+ /// This will typically be the home page, but may be a longer URL if
+ /// that Relying Party considers the scope of its realm to be more specific.
+ /// The URL provided here must allow discovery of the Relying Party's
+ /// XRDS document that advertises its OpenID RP endpoint.</param>
+ /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param>
+ /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically
+ /// be the same as <paramref name="claimedIdentifier"/>.</param>
+ /// <param name="extensions">The extensions.</param>
+ public void SendUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) {
+ Requires.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
+ Requires.NotNull(providerEndpoint, "providerEndpoint");
+ Requires.True(providerEndpoint.IsAbsoluteUri, "providerEndpoint");
+ Requires.NotNull(relyingPartyRealm, "relyingPartyRealm");
+ Requires.NotNull(claimedIdentifier, "claimedIdentifier");
+ Requires.NotNull(localIdentifier, "localIdentifier");
+
+ this.PrepareUnsolicitedAssertion(providerEndpoint, relyingPartyRealm, claimedIdentifier, localIdentifier, extensions).Send();
+ }
+
+ /// <summary>
+ /// Prepares an identity assertion on behalf of one of this Provider's
+ /// members in order to redirect the user agent to a relying party
+ /// web site and log him/her in immediately in one uninterrupted step.
+ /// </summary>
+ /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param>
+ /// <param name="relyingPartyRealm">The URL of the Relying Party web site.
+ /// This will typically be the home page, but may be a longer URL if
+ /// that Relying Party considers the scope of its realm to be more specific.
+ /// The URL provided here must allow discovery of the Relying Party's
+ /// XRDS document that advertises its OpenID RP endpoint.</param>
+ /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param>
+ /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically
+ /// be the same as <paramref name="claimedIdentifier"/>.</param>
+ /// <param name="extensions">The extensions.</param>
+ /// <returns>
+ /// A <see cref="OutgoingWebResponse"/> object describing the HTTP response to send
+ /// the user agent to allow the redirect with assertion to happen.
+ /// </returns>
+ public OutgoingWebResponse PrepareUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) {
+ Requires.NotNull(providerEndpoint, "providerEndpoint");
+ Requires.True(providerEndpoint.IsAbsoluteUri, "providerEndpoint");
+ Requires.NotNull(relyingPartyRealm, "relyingPartyRealm");
+ Requires.NotNull(claimedIdentifier, "claimedIdentifier");
+ Requires.NotNull(localIdentifier, "localIdentifier");
+ Requires.ValidState(this.Channel.WebRequestHandler != null);
+
+ // Although the RP should do their due diligence to make sure that this OP
+ // is authorized to send an assertion for the given claimed identifier,
+ // do due diligence by performing our own discovery on the claimed identifier
+ // and make sure that it is tied to this OP and OP local identifier.
+ if (this.SecuritySettings.UnsolicitedAssertionVerification != ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.NeverVerify) {
+ var serviceEndpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null);
+ var discoveredEndpoints = this.RelyingParty.Discover(claimedIdentifier);
+ if (!discoveredEndpoints.Contains(serviceEndpoint)) {
+ Logger.OpenId.WarnFormat(
+ "Failed to send unsolicited assertion for {0} because its discovered services did not include this endpoint: {1}{2}{1}Discovered endpoints: {1}{3}",
+ claimedIdentifier,
+ Environment.NewLine,
+ serviceEndpoint,
+ discoveredEndpoints.ToStringDeferred(true));
+
+ // Only FAIL if the setting is set for it.
+ if (this.securitySettings.UnsolicitedAssertionVerification == ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess) {
+ ErrorUtilities.ThrowProtocol(OpenIdStrings.UnsolicitedAssertionForUnrelatedClaimedIdentifier, claimedIdentifier);
+ }
+ }
+ }
+
+ Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier);
+ RelyingPartyEndpointDescription returnToEndpoint = null;
+ var returnToEndpoints = relyingPartyRealm.DiscoverReturnToEndpoints(this.WebRequestHandler, true);
+ if (returnToEndpoints != null) {
+ returnToEndpoint = returnToEndpoints.FirstOrDefault();
+ }
+ ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm);
+
+ var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) {
+ ProviderEndpoint = providerEndpoint,
+ ClaimedIdentifier = claimedIdentifier,
+ LocalIdentifier = localIdentifier,
+ };
+
+ if (extensions != null) {
+ foreach (IExtensionMessage extension in extensions) {
+ positiveAssertion.Extensions.Add(extension);
+ }
+ }
+
+ Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion");
+ return this.Channel.PrepareResponse(positiveAssertion);
+ }
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ private void Dispose(bool disposing) {
+ if (disposing) {
+ // Tear off the instance member as a local variable for thread safety.
+ IDisposable channel = this.Channel as IDisposable;
+ if (channel != null) {
+ channel.Dispose();
+ }
+
+ if (this.relyingParty != null) {
+ this.relyingParty.Dispose();
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Applies all behaviors to the response message.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private void ApplyBehaviorsToResponse(IRequest request) {
+ var authRequest = request as IAuthenticationRequest;
+ if (authRequest != null) {
+ foreach (var behavior in this.Behaviors) {
+ if (behavior.OnOutgoingResponse(authRequest)) {
+ // This behavior matched this request.
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Prepares the return value for the GetRequest method in the event of an exception.
+ /// </summary>
+ /// <param name="ex">The exception that forms the basis of the error response. Must not be null.</param>
+ /// <param name="httpRequestInfo">The incoming HTTP request. Must not be null.</param>
+ /// <param name="incomingMessage">The incoming message. May be null in the case that it was malformed.</param>
+ /// <returns>
+ /// Either the <see cref="IRequest"/> to return to the host site or null to indicate no response could be reasonably created and that the caller should rethrow the exception.
+ /// </returns>
+ private IRequest GetErrorResponse(ProtocolException ex, HttpRequestInfo httpRequestInfo, IDirectedProtocolMessage incomingMessage) {
+ Requires.NotNull(ex, "ex");
+ Requires.NotNull(httpRequestInfo, "httpRequestInfo");
+
+ Logger.OpenId.Error("An exception was generated while processing an incoming OpenID request.", ex);
+ IErrorMessage errorMessage;
+
+ // We must create the appropriate error message type (direct vs. indirect)
+ // based on what we see in the request.
+ string returnTo = httpRequestInfo.QueryString[Protocol.Default.openid.return_to];
+ if (returnTo != null) {
+ // An indirect request message from the RP
+ // We need to return an indirect response error message so the RP can consume it.
+ // Consistent with OpenID 2.0 section 5.2.3.
+ var indirectRequest = incomingMessage as SignedResponseRequest;
+ if (indirectRequest != null) {
+ errorMessage = new IndirectErrorResponse(indirectRequest);
+ } else {
+ errorMessage = new IndirectErrorResponse(Protocol.Default.Version, new Uri(returnTo));
+ }
+ } else if (httpRequestInfo.HttpMethod == "POST") {
+ // A direct request message from the RP
+ // We need to return a direct response error message so the RP can consume it.
+ // Consistent with OpenID 2.0 section 5.1.2.2.
+ errorMessage = new DirectErrorResponse(Protocol.Default.Version, incomingMessage);
+ } else {
+ // This may be an indirect request from an RP that was so badly
+ // formed that we cannot even return an error to the RP.
+ // The best we can do is display an error to the user.
+ // Returning null cues the caller to "throw;"
+ return null;
+ }
+
+ errorMessage.ErrorMessage = ex.ToStringDescriptive();
+
+ // Allow host to log this error and issue a ticket #.
+ // We tear off the field to a local var for thread safety.
+ IErrorReporting hostErrorHandler = this.ErrorReporting;
+ if (hostErrorHandler != null) {
+ errorMessage.Contact = hostErrorHandler.Contact;
+ errorMessage.Reference = hostErrorHandler.LogError(ex);
+ }
+
+ if (incomingMessage != null) {
+ return new AutoResponsiveRequest(incomingMessage, errorMessage, this.SecuritySettings);
+ } else {
+ return new AutoResponsiveRequest(errorMessage, this.SecuritySettings);
+ }
+ }
+
+ /// <summary>
+ /// Called by derived classes when behaviors are added or removed.
+ /// </summary>
+ /// <param name="sender">The collection being modified.</param>
+ /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
+ private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) {
+ foreach (IProviderBehavior profile in e.NewItems) {
+ profile.ApplySecuritySettings(this.SecuritySettings);
+ Reporting.RecordFeatureUse(profile);
+ }
+ }
+
+ /// <summary>
+ /// Provides a single OP association store instance that can handle switching between
+ /// association handle encoding modes.
+ /// </summary>
+ private class SwitchingAssociationStore : IProviderAssociationStore {
+ /// <summary>
+ /// The security settings of the Provider.
+ /// </summary>
+ private readonly ProviderSecuritySettings securitySettings;
+
+ /// <summary>
+ /// The association store that records association secrets in the association handles themselves.
+ /// </summary>
+ private IProviderAssociationStore associationHandleEncoder;
+
+ /// <summary>
+ /// The association store that records association secrets in a secret store.
+ /// </summary>
+ private IProviderAssociationStore associationSecretStorage;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SwitchingAssociationStore"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal SwitchingAssociationStore(ICryptoKeyStore cryptoKeyStore, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNull(securitySettings, "securitySettings");
+ this.securitySettings = securitySettings;
+
+ this.associationHandleEncoder = new ProviderAssociationHandleEncoder(cryptoKeyStore);
+ this.associationSecretStorage = new ProviderAssociationKeyStorage(cryptoKeyStore);
+ }
+
+ /// <summary>
+ /// Gets the association store that applies given the Provider's current security settings.
+ /// </summary>
+ internal IProviderAssociationStore AssociationStore {
+ get { return this.securitySettings.EncodeAssociationSecretsInHandles ? this.associationHandleEncoder : this.associationSecretStorage; }
+ }
+
+ /// <summary>
+ /// Stores an association and returns a handle for it.
+ /// </summary>
+ /// <param name="secret">The association secret.</param>
+ /// <param name="expiresUtc">The UTC time that the association should expire.</param>
+ /// <param name="privateAssociation">A value indicating whether this is a private association.</param>
+ /// <returns>
+ /// The association handle that represents this association.
+ /// </returns>
+ public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ return this.AssociationStore.Serialize(secret, expiresUtc, privateAssociation);
+ }
+
+ /// <summary>
+ /// Retrieves an association given an association handle.
+ /// </summary>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed).
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
+ public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
+ return this.AssociationStore.Deserialize(containingMessage, isPrivateAssociation, handle);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProviderUtilities.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProviderUtilities.cs
new file mode 100644
index 0000000..cf525f1
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/OpenIdProviderUtilities.cs
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdProviderUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.Provider;
+
+ /// <summary>
+ /// Utility methods for OpenID Providers.
+ /// </summary>
+ internal static class OpenIdProviderUtilities {
+ /// <summary>
+ /// Called to create the Association based on a request previously given by the Relying Party.
+ /// </summary>
+ /// <param name="request">The prior request for an association.</param>
+ /// <param name="response">The response.</param>
+ /// <param name="associationStore">The Provider's association store.</param>
+ /// <param name="securitySettings">The security settings for the Provider. Should be <c>null</c> for Relying Parties.</param>
+ /// <returns>
+ /// The created association.
+ /// </returns>
+ /// <remarks>
+ /// The response message is updated to include the details of the created association by this method.
+ /// This method is called by both the Provider and the Relying Party, but actually performs
+ /// quite different operations in either scenario.
+ /// </remarks>
+ internal static Association CreateAssociation(AssociateRequest request, IAssociateSuccessfulResponseProvider response, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(response, "response");
+ Requires.NotNull(securitySettings, "securitySettings");
+
+ // We need to initialize some common properties based on the created association.
+ var association = response.CreateAssociationAtProvider(request, associationStore, securitySettings);
+ response.ExpiresIn = association.SecondsTillExpiration;
+ response.AssociationHandle = association.Handle;
+
+ return association;
+ }
+
+ /// <summary>
+ /// Determines whether the association with the specified handle is (still) valid.
+ /// </summary>
+ /// <param name="associationStore">The association store.</param>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// <c>true</c> if the specified containing message is valid; otherwise, <c>false</c>.
+ /// </returns>
+ internal static bool IsValid(this IProviderAssociationStore associationStore, IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
+ Requires.NotNull(associationStore, "associationStore");
+ Requires.NotNull(containingMessage, "containingMessage");
+ Requires.NotNullOrEmpty(handle, "handle");
+ try {
+ return associationStore.Deserialize(containingMessage, isPrivateAssociation, handle) != null;
+ } catch (ProtocolException) {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs
new file mode 100644
index 0000000..9ebae1d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs
@@ -0,0 +1,223 @@
+//-----------------------------------------------------------------------
+// <copyright file="PrivatePersonalIdentifierProviderBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Provides standard PPID Identifiers to users to protect their identity from individual relying parties
+ /// and from colluding groups of relying parties.
+ /// </summary>
+ public abstract class PrivatePersonalIdentifierProviderBase : IDirectedIdentityIdentifierProvider {
+ /// <summary>
+ /// The type of hash function to use for the <see cref="Hasher"/> property.
+ /// </summary>
+ private const string HashAlgorithmName = "SHA256";
+
+ /// <summary>
+ /// The length of the salt to generate for first time PPID-users.
+ /// </summary>
+ private int newSaltLength = 20;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PrivatePersonalIdentifierProviderBase"/> class.
+ /// </summary>
+ /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param>
+ protected PrivatePersonalIdentifierProviderBase(Uri baseIdentifier) {
+ Requires.NotNull(baseIdentifier, "baseIdentifier");
+
+ this.Hasher = HashAlgorithm.Create(HashAlgorithmName);
+ this.Encoder = Encoding.UTF8;
+ this.BaseIdentifier = baseIdentifier;
+ this.PairwiseUnique = AudienceScope.Realm;
+ }
+
+ /// <summary>
+ /// A granularity description for who wide of an audience sees the same generated PPID.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Breaking change")]
+ public enum AudienceScope {
+ /// <summary>
+ /// A unique Identifier is generated for every realm. This is the highest security setting.
+ /// </summary>
+ Realm,
+
+ /// <summary>
+ /// Only the host name in the realm is used in calculating the PPID,
+ /// allowing for some level of sharing of the PPID Identifiers between RPs
+ /// that are able to share the same realm host value.
+ /// </summary>
+ RealmHost,
+
+ /// <summary>
+ /// Although the user's Identifier is still opaque to the RP so they cannot determine
+ /// who the user is at the OP, the same Identifier is used at all RPs so collusion
+ /// between the RPs is possible.
+ /// </summary>
+ Global,
+ }
+
+ /// <summary>
+ /// Gets the base URI on which to append the anonymous part.
+ /// </summary>
+ public Uri BaseIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether each Realm will get its own private identifier
+ /// for the authenticating uesr.
+ /// </summary>
+ /// <value>The default value is <see cref="AudienceScope.Realm"/>.</value>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Pairwise", Justification = "Meaningful word")]
+ public AudienceScope PairwiseUnique { get; set; }
+
+ /// <summary>
+ /// Gets the hash function to use to perform the one-way transform of a personal identifier
+ /// to an "anonymous" looking one.
+ /// </summary>
+ protected HashAlgorithm Hasher { get; private set; }
+
+ /// <summary>
+ /// Gets the encoder to use for transforming the personal identifier into bytes for hashing.
+ /// </summary>
+ protected Encoding Encoder { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the new length of the salt.
+ /// </summary>
+ /// <value>The new length of the salt.</value>
+ protected int NewSaltLength {
+ get {
+ return this.newSaltLength;
+ }
+
+ set {
+ Requires.InRange(value > 0, "value");
+ this.newSaltLength = value;
+ }
+ }
+
+ #region IDirectedIdentityIdentifierProvider Members
+
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ public Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
+ byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier);
+ string valueToHash = localIdentifier + "#";
+ switch (this.PairwiseUnique) {
+ case AudienceScope.Realm:
+ valueToHash += relyingPartyRealm;
+ break;
+ case AudienceScope.RealmHost:
+ valueToHash += relyingPartyRealm.Host;
+ break;
+ case AudienceScope.Global:
+ break;
+ default:
+ throw new InvalidOperationException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ OpenIdStrings.UnexpectedEnumPropertyValue,
+ "PairwiseUnique",
+ this.PairwiseUnique));
+ }
+
+ byte[] valueAsBytes = this.Encoder.GetBytes(valueToHash);
+ byte[] bytesToHash = new byte[valueAsBytes.Length + salt.Length];
+ valueAsBytes.CopyTo(bytesToHash, 0);
+ salt.CopyTo(bytesToHash, valueAsBytes.Length);
+ byte[] hash = this.Hasher.ComputeHash(bytesToHash);
+ string base64Hash = Convert.ToBase64String(hash);
+ Uri anonymousIdentifier = this.AppendIdentifiers(base64Hash);
+ return anonymousIdentifier;
+ }
+
+ /// <summary>
+ /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user.
+ /// </summary>
+ /// <param name="identifier">The identifier in question.</param>
+ /// <returns>
+ /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>.
+ /// </returns>
+ public virtual bool IsUserLocalIdentifier(Identifier identifier) {
+ return !identifier.ToString().StartsWith(this.BaseIdentifier.AbsoluteUri, StringComparison.Ordinal);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Creates a new salt to assign to a user.
+ /// </summary>
+ /// <returns>A non-null buffer of length <see cref="NewSaltLength"/> filled with a random salt.</returns>
+ protected virtual byte[] CreateSalt() {
+ // We COULD use a crypto random function, but for a salt it seems overkill.
+ return MessagingUtilities.GetNonCryptoRandomData(this.NewSaltLength);
+ }
+
+ /// <summary>
+ /// Creates a new PPID Identifier by appending a pseudonymous identifier suffix to
+ /// the <see cref="BaseIdentifier"/>.
+ /// </summary>
+ /// <param name="uriHash">The unique part of the Identifier to append to the common first part.</param>
+ /// <returns>The full PPID Identifier.</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "NOT equivalent overload. The recommended one breaks on relative URIs.")]
+ protected virtual Uri AppendIdentifiers(string uriHash) {
+ Requires.NotNullOrEmpty(uriHash, "uriHash");
+
+ if (string.IsNullOrEmpty(this.BaseIdentifier.Query)) {
+ // The uriHash will appear on the path itself.
+ string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_'));
+ return new Uri(this.BaseIdentifier, pathEncoded);
+ } else {
+ // The uriHash will appear on the query string.
+ string dataEncoded = Uri.EscapeDataString(uriHash);
+ return new Uri(this.BaseIdentifier + dataEncoded);
+ }
+ }
+
+ /// <summary>
+ /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier.</param>
+ /// <returns>The salt to use in the hash.</returns>
+ /// <remarks>
+ /// It is important that this method always return the same value for a given
+ /// <paramref name="localIdentifier"/>.
+ /// New salts can be generated for local identifiers without previously assigned salt
+ /// values by calling <see cref="CreateSalt"/> or by a custom method.
+ /// </remarks>
+ protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier);
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.Hasher != null);
+ Contract.Invariant(this.Encoder != null);
+ Contract.Invariant(this.BaseIdentifier != null);
+ Contract.Invariant(this.NewSaltLength > 0);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs
new file mode 100644
index 0000000..0e7c174
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs
@@ -0,0 +1,84 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProviderAssociationHandleEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Threading;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// Provides association storage in the association handle itself, but embedding signed and encrypted association
+ /// details in the handle.
+ /// </summary>
+ public class ProviderAssociationHandleEncoder : IProviderAssociationStore {
+ /// <summary>
+ /// The name of the bucket in which to store keys that encrypt association data into association handles.
+ /// </summary>
+ internal const string AssociationHandleEncodingSecretBucket = "https://localhost/dnoa/association_handles";
+
+ /// <summary>
+ /// The crypto key store used to persist encryption keys.
+ /// </summary>
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderAssociationHandleEncoder"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ public ProviderAssociationHandleEncoder(ICryptoKeyStore cryptoKeyStore) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ this.cryptoKeyStore = cryptoKeyStore;
+ }
+
+ /// <summary>
+ /// Encodes the specified association data bag.
+ /// </summary>
+ /// <param name="secret">The symmetric secret.</param>
+ /// <param name="expiresUtc">The UTC time that the association should expire.</param>
+ /// <param name="privateAssociation">A value indicating whether this is a private association.</param>
+ /// <returns>
+ /// The association handle that represents this association.
+ /// </returns>
+ public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ var associationDataBag = new AssociationDataBag {
+ Secret = secret,
+ IsPrivateAssociation = privateAssociation,
+ ExpiresUtc = expiresUtc,
+ };
+
+ var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow);
+ return formatter.Serialize(associationDataBag);
+ }
+
+ /// <summary>
+ /// Retrieves an association given an association handle.
+ /// </summary>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="privateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed).
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
+ public Association Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle) {
+ var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket);
+ AssociationDataBag bag;
+ try {
+ bag = formatter.Deserialize(containingMessage, handle);
+ } catch (ProtocolException ex) {
+ Logger.OpenId.Error("Rejecting an association because deserialization of the encoded handle failed.", ex);
+ return null;
+ }
+
+ ErrorUtilities.VerifyProtocol(bag.IsPrivateAssociation == privateAssociation, "Unexpected association type.");
+ Association assoc = Association.Deserialize(handle, bag.ExpiresUtc, bag.Secret);
+ return assoc.IsExpired ? null : assoc;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationKeyStorage.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationKeyStorage.cs
new file mode 100644
index 0000000..179699a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationKeyStorage.cs
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProviderAssociationKeyStorage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An association storage mechanism that stores the association secrets in a private store,
+ /// and returns randomly generated association handles to refer to these secrets.
+ /// </summary>
+ internal class ProviderAssociationKeyStorage : IProviderAssociationStore {
+ /// <summary>
+ /// The bucket to use when recording shared associations.
+ /// </summary>
+ internal const string SharedAssociationBucket = "https://localhost/dnoa/shared_associations";
+
+ /// <summary>
+ /// The bucket to use when recording private associations.
+ /// </summary>
+ internal const string PrivateAssociationBucket = "https://localhost/dnoa/private_associations";
+
+ /// <summary>
+ /// The backing crypto key store.
+ /// </summary>
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderAssociationKeyStorage"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The store where association secrets will be recorded.</param>
+ internal ProviderAssociationKeyStorage(ICryptoKeyStore cryptoKeyStore) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ this.cryptoKeyStore = cryptoKeyStore;
+ }
+
+ /// <summary>
+ /// Stores an association and returns a handle for it.
+ /// </summary>
+ /// <param name="secret">The association secret.</param>
+ /// <param name="expiresUtc">The UTC time that the association should expire.</param>
+ /// <param name="privateAssociation">A value indicating whether this is a private association.</param>
+ /// <returns>
+ /// The association handle that represents this association.
+ /// </returns>
+ public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ string handle;
+ this.cryptoKeyStore.StoreKey(
+ privateAssociation ? PrivateAssociationBucket : SharedAssociationBucket,
+ handle = OpenIdUtilities.GenerateRandomAssociationHandle(),
+ new CryptoKey(secret, expiresUtc));
+ return handle;
+ }
+
+ /// <summary>
+ /// Retrieves an association given an association handle.
+ /// </summary>
+ /// <param name="containingMessage">The OpenID message that referenced this association handle.</param>
+ /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param>
+ /// <param name="handle">The association handle.</param>
+ /// <returns>
+ /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed).
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
+ public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
+ var key = this.cryptoKeyStore.GetKey(isPrivateAssociation ? PrivateAssociationBucket : SharedAssociationBucket, handle);
+ if (key != null) {
+ return Association.Deserialize(handle, key.ExpiresUtc, key.Key);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Request.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Request.cs
new file mode 100644
index 0000000..c5b6dac
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/Request.cs
@@ -0,0 +1,210 @@
+//-----------------------------------------------------------------------
+// <copyright file="Request.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Implements the <see cref="IRequest"/> interface for all incoming
+ /// request messages to an OpenID Provider.
+ /// </summary>
+ [Serializable]
+ [ContractClass(typeof(RequestContract))]
+ [ContractVerification(true)]
+ internal abstract class Request : IRequest {
+ /// <summary>
+ /// The incoming request message.
+ /// </summary>
+ private readonly IDirectedProtocolMessage request;
+
+ /// <summary>
+ /// The incoming request message cast to its extensible form.
+ /// Or null if the message does not support extensions.
+ /// </summary>
+ private readonly IProtocolMessageWithExtensions extensibleMessage;
+
+ /// <summary>
+ /// The version of the OpenID protocol to use.
+ /// </summary>
+ private readonly Version protocolVersion;
+
+ /// <summary>
+ /// Backing store for the <see cref="Protocol"/> property.
+ /// </summary>
+ [NonSerialized]
+ private Protocol protocol;
+
+ /// <summary>
+ /// The list of extensions to add to the response message.
+ /// </summary>
+ private List<IOpenIdMessageExtension> responseExtensions = new List<IOpenIdMessageExtension>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Request"/> class.
+ /// </summary>
+ /// <param name="request">The incoming request message.</param>
+ /// <param name="securitySettings">The security settings from the channel.</param>
+ protected Request(IDirectedProtocolMessage request, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(securitySettings, "securitySettings");
+
+ this.request = request;
+ this.SecuritySettings = securitySettings;
+ this.protocolVersion = this.request.Version;
+ this.extensibleMessage = request as IProtocolMessageWithExtensions;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Request"/> class.
+ /// </summary>
+ /// <param name="version">The version.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ protected Request(Version version, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(version, "version");
+ Requires.NotNull(securitySettings, "securitySettings");
+
+ this.protocolVersion = version;
+ this.SecuritySettings = securitySettings;
+ }
+
+ #region IRequest Properties
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be sent to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// This property returns false if there are properties that must be set on this
+ /// request instance before the response can be sent.
+ /// </remarks>
+ public abstract bool IsResponseReady { get; }
+
+ /// <summary>
+ /// Gets or sets the security settings that apply to this request.
+ /// </summary>
+ /// <value>Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>.</value>
+ public ProviderSecuritySettings SecuritySettings { get; set; }
+
+ /// <summary>
+ /// Gets the response to send to the user agent.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="IsResponseReady"/> is <c>false</c>.</exception>
+ internal IProtocolMessage Response {
+ get {
+ Requires.ValidState(this.IsResponseReady, OpenIdStrings.ResponseNotReady);
+ Contract.Ensures(Contract.Result<IProtocolMessage>() != null);
+
+ if (this.responseExtensions.Count > 0) {
+ var extensibleResponse = this.ResponseMessage as IProtocolMessageWithExtensions;
+ ErrorUtilities.VerifyOperation(extensibleResponse != null, MessagingStrings.MessageNotExtensible, this.ResponseMessage.GetType().Name);
+ foreach (var extension in this.responseExtensions) {
+ // It's possible that a prior call to this property
+ // has already added some/all of the extensions to the message.
+ // We don't have to worry about deleting old ones because
+ // this class provides no facility for removing extensions
+ // that are previously added.
+ if (!extensibleResponse.Extensions.Contains(extension)) {
+ extensibleResponse.Extensions.Add(extension);
+ }
+ }
+ }
+
+ return this.ResponseMessage;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the original request message.
+ /// </summary>
+ /// <value>This may be null in the case of an unrecognizable message.</value>
+ protected internal IDirectedProtocolMessage RequestMessage {
+ get { return this.request; }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ protected abstract IProtocolMessage ResponseMessage { get; }
+
+ /// <summary>
+ /// Gets the protocol version used in the request.
+ /// </summary>
+ protected Protocol Protocol {
+ get {
+ if (this.protocol == null) {
+ this.protocol = Protocol.Lookup(this.protocolVersion);
+ }
+
+ return this.protocol;
+ }
+ }
+
+ #region IRequest Methods
+
+ /// <summary>
+ /// Adds an extension to the response to send to the relying party.
+ /// </summary>
+ /// <param name="extension">The extension to add to the response message.</param>
+ public void AddResponseExtension(IOpenIdMessageExtension extension) {
+ // Because the derived AuthenticationRequest class can swap out
+ // one response message for another (auth vs. no-auth), and because
+ // some response messages support extensions while others don't,
+ // we just add the extensions to a collection here and add them
+ // to the response on the way out.
+ this.responseExtensions.Add(extension);
+ }
+
+ /// <summary>
+ /// Removes any response extensions previously added using <see cref="AddResponseExtension"/>.
+ /// </summary>
+ /// <remarks>
+ /// This should be called before sending a negative response back to the relying party
+ /// if extensions were already added, since negative responses cannot carry extensions.
+ /// </remarks>
+ public void ClearResponseExtensions() {
+ this.responseExtensions.Clear();
+ }
+
+ /// <summary>
+ /// Gets an extension sent from the relying party.
+ /// </summary>
+ /// <typeparam name="T">The type of the extension.</typeparam>
+ /// <returns>
+ /// An instance of the extension initialized with values passed in with the request.
+ /// </returns>
+ public T GetExtension<T>() where T : IOpenIdMessageExtension, new() {
+ if (this.extensibleMessage != null) {
+ return this.extensibleMessage.Extensions.OfType<T>().SingleOrDefault();
+ } else {
+ return default(T);
+ }
+ }
+
+ /// <summary>
+ /// Gets an extension sent from the relying party.
+ /// </summary>
+ /// <param name="extensionType">The type of the extension.</param>
+ /// <returns>
+ /// An instance of the extension initialized with values passed in with the request.
+ /// </returns>
+ public IOpenIdMessageExtension GetExtension(Type extensionType) {
+ if (this.extensibleMessage != null) {
+ return this.extensibleMessage.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).SingleOrDefault();
+ } else {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/RequestContract.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/RequestContract.cs
new file mode 100644
index 0000000..ae7104c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/RequestContract.cs
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------
+// <copyright file="RequestContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Code contract for the <see cref="Request"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(Request))]
+ internal abstract class RequestContract : Request {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="RequestContract"/> class from being created.
+ /// </summary>
+ private RequestContract() : base((Version)null, null) {
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be sent to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// This property returns false if there are properties that must be set on this
+ /// request instance before the response can be sent.
+ /// </remarks>
+ public override bool IsResponseReady {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>.
+ /// </summary>
+ protected override IProtocolMessage ResponseMessage {
+ get {
+ Requires.ValidState(this.IsResponseReady);
+ Contract.Ensures(Contract.Result<IProtocolMessage>() != null);
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/StandardProviderApplicationStore.cs
new file mode 100644
index 0000000..b5880d3
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/StandardProviderApplicationStore.cs
@@ -0,0 +1,117 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardProviderApplicationStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An in-memory store for Providers, suitable for single server, single process
+ /// ASP.NET web sites.
+ /// </summary>
+ /// <remarks>
+ /// This class provides only a basic implementation that is likely to work
+ /// out of the box on most single-server web sites. It is highly recommended
+ /// that high traffic web sites consider using a database to store the information
+ /// used by an OpenID Provider and write a custom implementation of the
+ /// <see cref="IOpenIdApplicationStore"/> interface to use instead of this
+ /// class.
+ /// </remarks>
+ public class StandardProviderApplicationStore : IOpenIdApplicationStore {
+ /// <summary>
+ /// The nonce store to use.
+ /// </summary>
+ private readonly INonceStore nonceStore;
+
+ /// <summary>
+ /// The crypto key store where symmetric keys are persisted.
+ /// </summary>
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardProviderApplicationStore"/> class.
+ /// </summary>
+ public StandardProviderApplicationStore() {
+ this.nonceStore = new NonceMemoryStore(OpenIdElement.Configuration.MaxAuthenticationTime);
+ this.cryptoKeyStore = new MemoryCryptoKeyStore();
+ }
+
+ #region INonceStore Members
+
+ /// <summary>
+ /// Stores a given nonce and timestamp.
+ /// </summary>
+ /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param>
+ /// <param name="nonce">A series of random characters.</param>
+ /// <param name="timestampUtc">The timestamp that together with the nonce string make it unique.
+ /// The timestamp may also be used by the data store to clear out old nonces.</param>
+ /// <returns>
+ /// True if the nonce+timestamp (combination) was not previously in the database.
+ /// False if the nonce was stored previously with the same timestamp.
+ /// </returns>
+ /// <remarks>
+ /// The nonce must be stored for no less than the maximum time window a message may
+ /// be processed within before being discarded as an expired message.
+ /// If the binding element is applicable to your channel, this expiration window
+ /// is retrieved or set using the
+ /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property.
+ /// </remarks>
+ public bool StoreNonce(string context, string nonce, DateTime timestampUtc) {
+ return this.nonceStore.StoreNonce(context, nonce, timestampUtc);
+ }
+
+ #endregion
+
+ #region ICryptoKeyStore
+
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ return this.cryptoKeyStore.GetKey(bucket, handle);
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return this.cryptoKeyStore.GetKeys(bucket);
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ this.cryptoKeyStore.StoreKey(bucket, handle, key);
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ this.cryptoKeyStore.RemoveKey(bucket, handle);
+ }
+
+ #endregion
+ }
+}