diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider')
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, & 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<T>()"/> 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> + /// <Service xmlns="xri://$xrd*($v*2.0)"> + /// <Type>http://specs.openid.net/extensions/ui/icon</Type> + /// <URI>http://consumer.example.com/images/image.jpg</URI> + /// </Service> + /// </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 + } +} |