diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty')
22 files changed, 4349 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs new file mode 100644 index 0000000..eb501c5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs @@ -0,0 +1,246 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net; + using System.Security; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Manages the establishment, storage and retrieval of associations at the relying party. + /// </summary> + internal class AssociationManager { + /// <summary> + /// The storage to use for saving and retrieving associations. May be null. + /// </summary> + private readonly IRelyingPartyAssociationStore associationStore; + + /// <summary> + /// Backing field for the <see cref="Channel"/> property. + /// </summary> + private Channel channel; + + /// <summary> + /// Backing field for the <see cref="SecuritySettings"/> property. + /// </summary> + private RelyingPartySecuritySettings securitySettings; + + /// <summary> + /// Initializes a new instance of the <see cref="AssociationManager"/> class. + /// </summary> + /// <param name="channel">The channel the relying party is using.</param> + /// <param name="associationStore">The association store. May be null for dumb mode relying parties.</param> + /// <param name="securitySettings">The security settings.</param> + internal AssociationManager(Channel channel, IRelyingPartyAssociationStore associationStore, RelyingPartySecuritySettings securitySettings) { + Requires.NotNull(channel, "channel"); + Requires.NotNull(securitySettings, "securitySettings"); + + this.channel = channel; + this.associationStore = associationStore; + this.securitySettings = securitySettings; + } + + /// <summary> + /// Gets or sets the channel to use for establishing associations. + /// </summary> + /// <value>The channel.</value> + internal Channel Channel { + get { + return this.channel; + } + + set { + Requires.NotNull(value, "value"); + this.channel = value; + } + } + + /// <summary> + /// Gets or sets the security settings to apply in choosing association types to support. + /// </summary> + internal RelyingPartySecuritySettings SecuritySettings { + get { + return this.securitySettings; + } + + set { + Requires.NotNull(value, "value"); + this.securitySettings = value; + } + } + + /// <summary> + /// Gets a value indicating whether this instance has an association store. + /// </summary> + /// <value> + /// <c>true</c> if the relying party can act in 'smart' mode; + /// <c>false</c> if the relying party must always act in 'dumb' mode. + /// </value> + internal bool HasAssociationStore { + get { return this.associationStore != null; } + } + + /// <summary> + /// Gets the storage to use for saving and retrieving associations. May be null. + /// </summary> + internal IRelyingPartyAssociationStore AssociationStoreTestHook { + get { return this.associationStore; } + } + + /// <summary> + /// Gets an association between this Relying Party and a given Provider + /// if it already exists in the association store. + /// </summary> + /// <param name="provider">The provider to create an association with.</param> + /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns> + internal Association GetExistingAssociation(IProviderEndpoint provider) { + Requires.NotNull(provider, "provider"); + + // If the RP has no application store for associations, there's no point in creating one. + if (this.associationStore == null) { + return null; + } + + Association association = this.associationStore.GetAssociation(provider.Uri, this.SecuritySettings); + + // If the returned association does not fulfill security requirements, ignore it. + if (association != null && !this.SecuritySettings.IsAssociationInPermittedRange(association)) { + association = null; + } + + if (association != null && !association.HasUsefulLifeRemaining) { + association = null; + } + + return association; + } + + /// <summary> + /// Gets an existing association with the specified Provider, or attempts to create + /// a new association of one does not already exist. + /// </summary> + /// <param name="provider">The provider to get an association for.</param> + /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns> + internal Association GetOrCreateAssociation(IProviderEndpoint provider) { + return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider); + } + + /// <summary> + /// Creates a new association with a given Provider. + /// </summary> + /// <param name="provider">The provider to create an association with.</param> + /// <returns> + /// The newly created association, or null if no association can be created with + /// the given Provider given the current security settings. + /// </returns> + /// <remarks> + /// A new association is created and returned even if one already exists in the + /// association store. + /// Any new association is automatically added to the <see cref="associationStore"/>. + /// </remarks> + private Association CreateNewAssociation(IProviderEndpoint provider) { + Requires.NotNull(provider, "provider"); + + // If there is no association store, there is no point in creating an association. + if (this.associationStore == null) { + return null; + } + + try { + var associateRequest = AssociateRequestRelyingParty.Create(this.securitySettings, provider); + + const int RenegotiateRetries = 1; + return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries); + } catch (VerificationException ex) { + // See Trac ticket #163. In partial trust host environments, the + // Diffie-Hellman implementation we're using for HTTP OP endpoints + // sometimes causes the CLR to throw: + // "VerificationException: Operation could destabilize the runtime." + // Just give up and use dumb mode in this case. + Logger.OpenId.ErrorFormat("VerificationException occurred while trying to create an association with {0}. {1}", provider.Uri, ex); + return null; + } + } + + /// <summary> + /// Creates a new association with a given Provider. + /// </summary> + /// <param name="provider">The provider to create an association with.</param> + /// <param name="associateRequest">The associate request. May be <c>null</c>, which will always result in a <c>null</c> return value..</param> + /// <param name="retriesRemaining">The number of times to try the associate request again if the Provider suggests it.</param> + /// <returns> + /// The newly created association, or null if no association can be created with + /// the given Provider given the current security settings. + /// </returns> + private Association CreateNewAssociation(IProviderEndpoint provider, AssociateRequest associateRequest, int retriesRemaining) { + Requires.NotNull(provider, "provider"); + + if (associateRequest == null || retriesRemaining < 0) { + // this can happen if security requirements and protocol conflict + // to where there are no association types to choose from. + return null; + } + + try { + var associateResponse = this.channel.Request(associateRequest); + var associateSuccessfulResponse = associateResponse as IAssociateSuccessfulResponseRelyingParty; + var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse; + if (associateSuccessfulResponse != null) { + Association association = associateSuccessfulResponse.CreateAssociationAtRelyingParty(associateRequest); + this.associationStore.StoreAssociation(provider.Uri, association); + return association; + } else if (associateUnsuccessfulResponse != null) { + if (string.IsNullOrEmpty(associateUnsuccessfulResponse.AssociationType)) { + Logger.OpenId.Debug("Provider rejected an association request and gave no suggestion as to an alternative association type. Giving up."); + return null; + } + + if (!this.securitySettings.IsAssociationInPermittedRange(Protocol.Lookup(provider.Version), associateUnsuccessfulResponse.AssociationType)) { + Logger.OpenId.DebugFormat("Provider rejected an association request and suggested '{0}' as an association to try, which this Relying Party does not support. Giving up.", associateUnsuccessfulResponse.AssociationType); + return null; + } + + if (retriesRemaining <= 0) { + Logger.OpenId.Debug("Unable to agree on an association type with the Provider in the allowed number of retries. Giving up."); + return null; + } + + // Make sure the Provider isn't suggesting an incompatible pair of association/session types. + Protocol protocol = Protocol.Lookup(provider.Version); + ErrorUtilities.VerifyProtocol( + HmacShaAssociation.IsDHSessionCompatible(protocol, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType), + OpenIdStrings.IncompatibleAssociationAndSessionTypes, + associateUnsuccessfulResponse.AssociationType, + associateUnsuccessfulResponse.SessionType); + + associateRequest = AssociateRequestRelyingParty.Create(this.securitySettings, provider, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType); + return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1); + } else { + throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany); + } + } catch (ProtocolException ex) { + // If the association failed because the remote server can't handle Expect: 100 Continue headers, + // then our web request handler should have already accomodated for future calls. Go ahead and + // immediately make one of those future calls now to try to get the association to succeed. + if (StandardWebRequestHandler.IsExceptionFrom417ExpectationFailed(ex)) { + return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1); + } + + // Since having associations with OPs is not totally critical, we'll log and eat + // the exception so that auth may continue in dumb mode. + Logger.OpenId.ErrorFormat("An error occurred while trying to create an association with {0}. {1}", provider.Uri, ex); + return null; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs new file mode 100644 index 0000000..9f4a21f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationPreference.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Text; + + /// <summary> + /// Preferences regarding creation and use of an association between a relying party + /// and provider for authentication. + /// </summary> + internal enum AssociationPreference { + /// <summary> + /// Indicates that an association should be created for use in authentication + /// if one has not already been established between the relying party and the + /// selected provider. + /// </summary> + /// <remarks> + /// Even with this value, if an association attempt fails or the relying party + /// has no application store to recall associations, the authentication may + /// proceed without an association. + /// </remarks> + IfPossible, + + /// <summary> + /// Indicates that an association should be used for authentication only if + /// it happens to already exist. + /// </summary> + IfAlreadyEstablished, + + /// <summary> + /// Indicates that an authentication attempt should NOT use an OpenID association + /// between the relying party and the provider, even if an association was previously + /// created. + /// </summary> + Never, + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs new file mode 100644 index 0000000..e65750f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs @@ -0,0 +1,127 @@ +//----------------------------------------------------------------------- +// <copyright file="Associations.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A dictionary of handle/Association pairs. + /// </summary> + /// <remarks> + /// Each method is locked, even if it is only one line, so that they are thread safe + /// against each other, particularly the ones that enumerate over the list, since they + /// can break if the collection is changed by another thread during enumeration. + /// </remarks> + [DebuggerDisplay("Count = {assocs.Count}")] + [ContractVerification(true)] + internal class Associations { + /// <summary> + /// The lookup table where keys are the association handles and values are the associations themselves. + /// </summary> + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + private readonly KeyedCollection<string, Association> associations = new KeyedCollectionDelegate<string, Association>(assoc => assoc.Handle); + + /// <summary> + /// Initializes a new instance of the <see cref="Associations"/> class. + /// </summary> + public Associations() { + } + + /// <summary> + /// Gets the <see cref="Association"/>s ordered in order of descending issue date + /// (most recently issued comes first). An empty sequence if no valid associations exist. + /// </summary> + /// <remarks> + /// This property is used by relying parties that are initiating authentication requests. + /// It does not apply to Providers, which always need a specific association by handle. + /// </remarks> + public IEnumerable<Association> Best { + get { + Contract.Ensures(Contract.Result<IEnumerable<Association>>() != null); + + lock (this.associations) { + return this.associations.OrderByDescending(assoc => assoc.Issued); + } + } + } + + /// <summary> + /// Stores an <see cref="Association"/> in the collection. + /// </summary> + /// <param name="association">The association to add to the collection.</param> + public void Set(Association association) { + Requires.NotNull(association, "association"); + Contract.Ensures(this.Get(association.Handle) == association); + lock (this.associations) { + this.associations.Remove(association.Handle); // just in case one already exists. + this.associations.Add(association); + } + + Contract.Assume(this.Get(association.Handle) == association); + } + + /// <summary> + /// Returns the <see cref="Association"/> with the given handle. Null if not found. + /// </summary> + /// <param name="handle">The handle to the required association.</param> + /// <returns>The desired association, or null if none with the given handle could be found.</returns> + [Pure] + public Association Get(string handle) { + Requires.NotNullOrEmpty(handle, "handle"); + + lock (this.associations) { + if (this.associations.Contains(handle)) { + return this.associations[handle]; + } else { + return null; + } + } + } + + /// <summary> + /// Removes the <see cref="Association"/> with the given handle. + /// </summary> + /// <param name="handle">The handle to the required association.</param> + /// <returns>Whether an <see cref="Association"/> with the given handle was in the collection for removal.</returns> + public bool Remove(string handle) { + Requires.NotNullOrEmpty(handle, "handle"); + lock (this.associations) { + return this.associations.Remove(handle); + } + } + + /// <summary> + /// Removes all expired associations from the collection. + /// </summary> + public void ClearExpired() { + lock (this.associations) { + var expireds = this.associations.Where(assoc => assoc.IsExpired).ToList(); + foreach (Association assoc in expireds) { + this.associations.Remove(assoc.Handle); + } + } + } + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.associations != null); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs new file mode 100644 index 0000000..c85c0c5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs @@ -0,0 +1,594 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using System.Threading; + using System.Web; + + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Facilitates customization and creation and an authentication request + /// that a Relying Party is preparing to send. + /// </summary> + internal class AuthenticationRequest : IAuthenticationRequest { + /// <summary> + /// The name of the internal callback parameter to use to store the user-supplied identifier. + /// </summary> + internal const string UserSuppliedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "userSuppliedIdentifier"; + + /// <summary> + /// The relying party that created this request object. + /// </summary> + private readonly OpenIdRelyingParty RelyingParty; + + /// <summary> + /// How an association may or should be created or used in the formulation of the + /// authentication request. + /// </summary> + private AssociationPreference associationPreference = AssociationPreference.IfPossible; + + /// <summary> + /// The extensions that have been added to this authentication request. + /// </summary> + private List<IOpenIdMessageExtension> extensions = new List<IOpenIdMessageExtension>(); + + /// <summary> + /// Arguments to add to the return_to part of the query string, so that + /// these values come back to the consumer when the user agent returns. + /// </summary> + private Dictionary<string, string> returnToArgs = new Dictionary<string, string>(); + + /// <summary> + /// A value indicating whether the return_to callback arguments must be signed. + /// </summary> + /// <remarks> + /// This field defaults to false, but is set to true as soon as the first callback argument + /// is added that indicates it must be signed. At which point, all arguments are signed + /// even if individual ones did not need to be. + /// </remarks> + private bool returnToArgsMustBeSigned; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class. + /// </summary> + /// <param name="discoveryResult">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param> + /// <param name="realm">The realm, or root URL, of the host web site.</param> + /// <param name="returnToUrl">The base return_to URL that the Provider should return the user to to complete authentication. This should not include callback parameters as these should be added using the <see cref="AddCallbackArguments(string, string)"/> method.</param> + /// <param name="relyingParty">The relying party that created this instance.</param> + private AuthenticationRequest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) { + Requires.NotNull(discoveryResult, "discoveryResult"); + Requires.NotNull(realm, "realm"); + Requires.NotNull(returnToUrl, "returnToUrl"); + Requires.NotNull(relyingParty, "relyingParty"); + + this.DiscoveryResult = discoveryResult; + this.RelyingParty = relyingParty; + this.Realm = realm; + this.ReturnToUrl = returnToUrl; + + this.Mode = AuthenticationRequestMode.Setup; + } + + #region IAuthenticationRequest Members + + /// <summary> + /// Gets or sets the mode the Provider should use during authentication. + /// </summary> + /// <value></value> + public AuthenticationRequestMode Mode { get; set; } + + /// <summary> + /// Gets the HTTP response the relying party should send to the user agent + /// to redirect it to the OpenID Provider to start the OpenID authentication process. + /// </summary> + /// <value></value> + public OutgoingWebResponse RedirectingResponse { + get { + foreach (var behavior in this.RelyingParty.Behaviors) { + behavior.OnOutgoingAuthenticationRequest(this); + } + + return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage()); + } + } + + /// <summary> + /// Gets the URL that the user agent will return to after authentication + /// completes or fails at the Provider. + /// </summary> + /// <value></value> + public Uri ReturnToUrl { get; private set; } + + /// <summary> + /// Gets the URL that identifies this consumer web application that + /// the Provider will display to the end user. + /// </summary> + public Realm Realm { get; private set; } + + /// <summary> + /// Gets the Claimed Identifier that the User Supplied Identifier + /// resolved to. Null if the user provided an OP Identifier + /// (directed identity). + /// </summary> + /// <value></value> + /// <remarks> + /// Null is returned if the user is using the directed identity feature + /// of OpenID 2.0 to make it nearly impossible for a relying party site + /// to improperly store the reserved OpenID URL used for directed identity + /// as a user's own Identifier. + /// However, to test for the Directed Identity feature, please test the + /// <see cref="IsDirectedIdentity"/> property rather than testing this + /// property for a null value. + /// </remarks> + public Identifier ClaimedIdentifier { + get { return this.IsDirectedIdentity ? null : this.DiscoveryResult.ClaimedIdentifier; } + } + + /// <summary> + /// Gets a value indicating whether the authenticating user has chosen to let the Provider + /// determine and send the ClaimedIdentifier after authentication. + /// </summary> + public bool IsDirectedIdentity { + get { return this.DiscoveryResult.ClaimedIdentifier == this.DiscoveryResult.Protocol.ClaimedIdentifierForOPIdentifier; } + } + + /// <summary> + /// Gets or sets a value indicating whether this request only carries extensions + /// and is not a request to verify that the user controls some identifier. + /// </summary> + /// <value> + /// <c>true</c> if this request is merely a carrier of extensions and is not + /// about an OpenID identifier; otherwise, <c>false</c>. + /// </value> + public bool IsExtensionOnly { get; set; } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + public IProviderEndpoint Provider { + get { return this.DiscoveryResult; } + } + + /// <summary> + /// Gets the discovery result leading to the formulation of this request. + /// </summary> + /// <value>The discovery result.</value> + public IdentifierDiscoveryResult DiscoveryResult { get; private set; } + + #endregion + + /// <summary> + /// Gets or sets how an association may or should be created or used + /// in the formulation of the authentication request. + /// </summary> + internal AssociationPreference AssociationPreference { + get { return this.associationPreference; } + set { this.associationPreference = value; } + } + + /// <summary> + /// Gets the extensions that have been added to the request. + /// </summary> + internal IEnumerable<IOpenIdMessageExtension> AppliedExtensions { + get { return this.extensions; } + } + + /// <summary> + /// Gets the list of extensions for this request. + /// </summary> + internal IList<IOpenIdMessageExtension> Extensions { + get { return this.extensions; } + } + + #region IAuthenticationRequest methods + + /// <summary> + /// Makes a dictionary of key/value pairs available when the authentication is completed. + /// </summary> + /// <param name="arguments">The arguments to add to the request's return_to URI.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping in transit. No + /// privacy-sensitive data should be stored using this method.</para> + /// <para>The values stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>, which will only return the value + /// if it hasn't been tampered with in transit.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void AddCallbackArguments(IDictionary<string, string> arguments) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + foreach (var pair in arguments) { + ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(pair.Key), MessagingStrings.UnexpectedNullOrEmptyKey); + ErrorUtilities.VerifyArgument(pair.Value != null, MessagingStrings.UnexpectedNullValue, pair.Key); + + this.returnToArgs.Add(pair.Key, pair.Value); + } + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping in transit. No + /// privacy-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>, which will only return the value + /// if it hasn't been tampered with in transit.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void AddCallbackArguments(string key, string value) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + this.returnToArgs.Add(key, value); + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against tampering in transit. No + /// security-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void SetCallbackArgument(string key, string value) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + this.returnToArgs[key] = value; + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed without + /// requiring a return_to signature to protect against tampering of the callback argument. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping or tampering in transit. No + /// security-sensitive data should be stored using this method. </para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void SetUntrustedCallbackArgument(string key, string value) { + this.returnToArgs[key] = value; + } + + /// <summary> + /// Adds an OpenID extension to the request directed at the OpenID provider. + /// </summary> + /// <param name="extension">The initialized extension to add to the request.</param> + public void AddExtension(IOpenIdMessageExtension extension) { + this.extensions.Add(extension); + } + + /// <summary> + /// Redirects the user agent to the provider for authentication. + /// </summary> + /// <remarks> + /// This method requires an ASP.NET HttpContext. + /// </remarks> + public void RedirectToProvider() { + this.RedirectingResponse.Send(); + } + + #endregion + + /// <summary> + /// Performs identifier discovery, creates associations and generates authentication requests + /// on-demand for as long as new ones can be generated based on the results of Identifier discovery. + /// </summary> + /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> + /// <param name="relyingParty">The relying party.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnToUrl">The return_to base URL.</param> + /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + internal static IEnumerable<AuthenticationRequest> Create(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.NotNull(relyingParty, "relyingParty"); + Requires.NotNull(realm, "realm"); + Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); + + // Normalize the portion of the return_to path that correlates to the realm for capitalization. + // (so that if a web app base path is /MyApp/, but the URL of this request happens to be + // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm. + UriBuilder returnTo = new UriBuilder(returnToUrl); + if (returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + !returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.Ordinal)) { + returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length); + returnToUrl = returnTo.Uri; + } + + userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment(); + if (relyingParty.SecuritySettings.RequireSsl) { + // Rather than check for successful SSL conversion at this stage, + // We'll wait for secure discovery to fail on the new identifier. + if (!userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier)) { + // But at least log the failure. + Logger.OpenId.WarnFormat("RequireSsl mode is on, so discovery on insecure identifier {0} will yield no results.", userSuppliedIdentifier); + } + } + + if (Logger.OpenId.IsWarnEnabled && returnToUrl.Query != null) { + NameValueCollection returnToArgs = HttpUtility.ParseQueryString(returnToUrl.Query); + foreach (string key in returnToArgs) { + if (OpenIdRelyingParty.IsOpenIdSupportingParameter(key)) { + Logger.OpenId.WarnFormat("OpenID argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key); + } + } + } + + // Throw an exception now if the realm and the return_to URLs don't match + // as required by the provider. We could wait for the provider to test this and + // fail, but this will be faster and give us a better error message. + ErrorUtilities.VerifyProtocol(realm.Contains(returnToUrl), OpenIdStrings.ReturnToNotUnderRealm, returnToUrl, realm); + + // Perform discovery right now (not deferred). + IEnumerable<IdentifierDiscoveryResult> serviceEndpoints; + try { + var results = relyingParty.Discover(userSuppliedIdentifier).CacheGeneratedResults(); + + // If any OP Identifier service elements were found, we must not proceed + // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2. + // For a discussion on this topic, see + // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8 + // Usually the Discover method we called will automatically filter this for us, but + // just to be sure, we'll do it here as well since the RP may be configured to allow + // these dual identifiers for assertion verification purposes. + var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier).CacheGeneratedResults(); + var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier); + serviceEndpoints = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers; + } catch (ProtocolException ex) { + Logger.Yadis.ErrorFormat("Error while performing discovery on: \"{0}\": {1}", userSuppliedIdentifier, ex); + serviceEndpoints = Enumerable.Empty<IdentifierDiscoveryResult>(); + } + + // Filter disallowed endpoints. + serviceEndpoints = relyingParty.SecuritySettings.FilterEndpoints(serviceEndpoints); + + // Call another method that defers request generation. + return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded); + } + + /// <summary> + /// Creates an instance of <see cref="AuthenticationRequest"/> FOR TESTING PURPOSES ONLY. + /// </summary> + /// <param name="discoveryResult">The discovery result.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnTo">The return to.</param> + /// <param name="rp">The relying party.</param> + /// <returns>The instantiated <see cref="AuthenticationRequest"/>.</returns> + internal static AuthenticationRequest CreateForTest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnTo, OpenIdRelyingParty rp) { + return new AuthenticationRequest(discoveryResult, realm, returnTo, rp); + } + + /// <summary> + /// Creates the request message to send to the Provider, + /// based on the properties in this instance. + /// </summary> + /// <returns>The message to send to the Provider.</returns> + internal SignedResponseRequest CreateRequestMessageTestHook() + { + return this.CreateRequestMessage(); + } + + /// <summary> + /// Performs deferred request generation for the <see cref="Create"/> method. + /// </summary> + /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> + /// <param name="relyingParty">The relying party.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnToUrl">The return_to base URL.</param> + /// <param name="serviceEndpoints">The discovered service endpoints on the Claimed Identifier.</param> + /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// All data validation and cleansing steps must have ALREADY taken place + /// before calling this method. + /// </remarks> + private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable<IdentifierDiscoveryResult> serviceEndpoints, bool createNewAssociationsAsNeeded) { + // DO NOT USE CODE CONTRACTS IN THIS METHOD, since it uses yield return + ErrorUtilities.VerifyArgumentNotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty"); + ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); + ErrorUtilities.VerifyArgumentNotNull(serviceEndpoints, "serviceEndpoints"); + ////Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); + + // If shared associations are required, then we had better have an association store. + ErrorUtilities.VerifyOperation(!relyingParty.SecuritySettings.RequireAssociation || relyingParty.AssociationManager.HasAssociationStore, OpenIdStrings.AssociationStoreRequired); + + Logger.Yadis.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier); + IEnumerable<IdentifierDiscoveryResult> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty); + + // Maintain a list of endpoints that we could not form an association with. + // We'll fallback to generating requests to these if the ones we CAN create + // an association with run out. + var failedAssociationEndpoints = new List<IdentifierDiscoveryResult>(0); + + foreach (var endpoint in endpoints) { + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); + + // The strategy here is to prefer endpoints with whom we can create associations. + Association association = null; + if (relyingParty.AssociationManager.HasAssociationStore) { + // In some scenarios (like the AJAX control wanting ALL auth requests possible), + // we don't want to create associations with every Provider. But we'll use + // associations where they are already formed from previous authentications. + association = createNewAssociationsAsNeeded ? relyingParty.AssociationManager.GetOrCreateAssociation(endpoint) : relyingParty.AssociationManager.GetExistingAssociation(endpoint); + if (association == null && createNewAssociationsAsNeeded) { + Logger.OpenId.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint.ProviderEndpoint); + + // No association could be created. Add it to the list of failed association + // endpoints and skip to the next available endpoint. + failedAssociationEndpoints.Add(endpoint); + continue; + } + } + + yield return new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty); + } + + // Now that we've run out of endpoints that respond to association requests, + // since we apparently are still running, the caller must want another request. + // We'll go ahead and generate the requests to OPs that may be down -- + // unless associations are set as required in our security settings. + if (failedAssociationEndpoints.Count > 0) { + if (relyingParty.SecuritySettings.RequireAssociation) { + Logger.OpenId.Warn("Associations could not be formed with some Providers. Security settings require shared associations for authentication requests so these will be skipped."); + } else { + Logger.OpenId.Debug("Now generating requests for Provider endpoints that failed initial association attempts."); + + foreach (var endpoint in failedAssociationEndpoints) { + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0} at endpoint: {1}", userSuppliedIdentifier, endpoint.ProviderEndpoint.AbsoluteUri); + + // Create the auth request, but prevent it from attempting to create an association + // because we've already tried. Let's not have it waste time trying again. + var authRequest = new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty); + authRequest.associationPreference = AssociationPreference.IfAlreadyEstablished; + yield return authRequest; + } + } + } + } + + /// <summary> + /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier. + /// </summary> + /// <param name="endpoints">The endpoints.</param> + /// <param name="relyingParty">The relying party.</param> + /// <returns>A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints.</returns> + private static List<IdentifierDiscoveryResult> FilterAndSortEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints, OpenIdRelyingParty relyingParty) { + Requires.NotNull(endpoints, "endpoints"); + Requires.NotNull(relyingParty, "relyingParty"); + + bool anyFilteredOut = false; + var filteredEndpoints = new List<IdentifierDiscoveryResult>(); + foreach (var endpoint in endpoints) { + if (relyingParty.FilterEndpoint(endpoint)) { + filteredEndpoints.Add(endpoint); + } else { + anyFilteredOut = true; + } + } + + // Sort endpoints so that the first one in the list is the most preferred one. + filteredEndpoints.OrderBy(ep => ep, relyingParty.EndpointOrder); + + var endpointList = new List<IdentifierDiscoveryResult>(filteredEndpoints.Count); + foreach (var endpoint in filteredEndpoints) { + endpointList.Add(endpoint); + } + + if (anyFilteredOut) { + Logger.Yadis.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count); + } + if (Logger.Yadis.IsDebugEnabled) { + if (MessagingUtilities.AreEquivalent(endpoints, endpointList)) { + Logger.Yadis.Debug("Filtering and sorting of endpoints did not affect the list."); + } else { + Logger.Yadis.Debug("After filtering and sorting service endpoints, this is the new prioritized list:"); + Logger.Yadis.Debug(Util.ToStringDeferred(filteredEndpoints, true)); + } + } + + return endpointList; + } + + /// <summary> + /// Creates the request message to send to the Provider, + /// based on the properties in this instance. + /// </summary> + /// <returns>The message to send to the Provider.</returns> + private SignedResponseRequest CreateRequestMessage() { + Association association = this.GetAssociation(); + + SignedResponseRequest request; + if (!this.IsExtensionOnly) { + CheckIdRequest authRequest = new CheckIdRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode); + authRequest.ClaimedIdentifier = this.DiscoveryResult.ClaimedIdentifier; + authRequest.LocalIdentifier = this.DiscoveryResult.ProviderLocalIdentifier; + request = authRequest; + } else { + request = new SignedResponseRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode); + } + request.Realm = this.Realm; + request.ReturnTo = this.ReturnToUrl; + request.AssociationHandle = association != null ? association.Handle : null; + request.SignReturnTo = this.returnToArgsMustBeSigned; + request.AddReturnToArguments(this.returnToArgs); + if (this.DiscoveryResult.UserSuppliedIdentifier != null && OpenIdElement.Configuration.RelyingParty.PreserveUserSuppliedIdentifier) { + request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.DiscoveryResult.UserSuppliedIdentifier.OriginalString); + } + foreach (IOpenIdMessageExtension extension in this.extensions) { + request.Extensions.Add(extension); + } + + return request; + } + + /// <summary> + /// Gets the association to use for this authentication request. + /// </summary> + /// <returns>The association to use; <c>null</c> to use 'dumb mode'.</returns> + private Association GetAssociation() { + Association association = null; + switch (this.associationPreference) { + case AssociationPreference.IfPossible: + association = this.RelyingParty.AssociationManager.GetOrCreateAssociation(this.DiscoveryResult); + if (association == null) { + // Avoid trying to create the association again if the redirecting response + // is generated again. + this.associationPreference = AssociationPreference.IfAlreadyEstablished; + } + break; + case AssociationPreference.IfAlreadyEstablished: + association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.DiscoveryResult); + break; + case AssociationPreference.Never: + break; + default: + throw new InternalErrorException(); + } + + return association; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/AXFetchAsSregTransform.cs new file mode 100644 index 0000000..b2794ec --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/AXFetchAsSregTransform.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// <copyright file="AXFetchAsSregTransform.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty.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.RelyingParty; + using DotNetOpenAuth.OpenId.RelyingParty.Extensions; + + /// <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, IRelyingPartyBehavior { + /// <summary> + /// Initializes a new instance of the <see cref="AXFetchAsSregTransform"/> class. + /// </summary> + public AXFetchAsSregTransform() { + } + + #region IRelyingPartyBehavior 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 IRelyingPartyBehavior.ApplySecuritySettings(RelyingPartySecuritySettings securitySettings) { + } + + /// <summary> + /// Called when an authentication request is about to be sent. + /// </summary> + /// <param name="request">The request.</param> + /// <remarks> + /// Implementations should be prepared to be called multiple times on the same outgoing message + /// without malfunctioning. + /// </remarks> + void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(RelyingParty.IAuthenticationRequest request) { + // Don't create AX extensions for OpenID 1.x messages, since AX requires OpenID 2.0. + if (request.Provider.Version.Major >= 2) { + request.SpreadSregToAX(this.AXFormats); + } + } + + /// <summary> + /// Called when an incoming positive assertion is received. + /// </summary> + /// <param name="assertion">The positive assertion.</param> + void IRelyingPartyBehavior.OnIncomingPositiveAssertion(IAuthenticationResponse assertion) { + if (assertion.GetExtension<ClaimsResponse>() == null) { + ClaimsResponse sreg = assertion.UnifyExtensionsAsSreg(true); + ((PositiveAnonymousResponse)assertion).Response.Extensions.Add(sreg); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/GsaIcamProfile.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/GsaIcamProfile.cs new file mode 100644 index 0000000..5a1ddaa --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Behaviors/GsaIcamProfile.cs @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------- +// <copyright file="GsaIcamProfile.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty.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.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, IRelyingPartyBehavior { + /// <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."); + } + } + + #region IRelyingPartyBehavior Members + + /// <summary> + /// Applies a well known set of security requirements. + /// </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 IRelyingPartyBehavior.ApplySecuritySettings(RelyingPartySecuritySettings securitySettings) { + if (securitySettings.MaximumHashBitLength < 256) { + securitySettings.MaximumHashBitLength = 256; + } + + securitySettings.RequireSsl = !DisableSslRequirement; + securitySettings.RequireDirectedIdentity = true; + securitySettings.RequireAssociation = true; + securitySettings.RejectDelegatingIdentifiers = true; + securitySettings.IgnoreUnsignedExtensions = true; + securitySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; + } + + /// <summary> + /// Called when an authentication request is about to be sent. + /// </summary> + /// <param name="request">The request.</param> + void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(RelyingParty.IAuthenticationRequest request) { + RelyingParty.AuthenticationRequest requestInternal = (RelyingParty.AuthenticationRequest)request; + ErrorUtilities.VerifyProtocol(string.Equals(request.Realm.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal) || DisableSslRequirement, BehaviorStrings.RealmMustBeHttps); + + var pape = requestInternal.AppliedExtensions.OfType<PolicyRequest>().SingleOrDefault(); + if (pape == null) { + request.AddExtension(pape = new PolicyRequest()); + } + + if (!pape.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + pape.PreferredPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier); + } + + if (!pape.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) { + pape.PreferredPolicies.Add(AuthenticationPolicies.USGovernmentTrustLevel1); + } + + if (!AllowPersonallyIdentifiableInformation && !pape.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) { + pape.PreferredPolicies.Add(AuthenticationPolicies.NoPersonallyIdentifiableInformation); + } + + if (pape.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) { + ErrorUtilities.VerifyProtocol( + (!requestInternal.AppliedExtensions.OfType<ClaimsRequest>().Any() && + !requestInternal.AppliedExtensions.OfType<FetchRequest>().Any()), + BehaviorStrings.PiiIncludedWithNoPiiPolicy); + } + + Reporting.RecordEventOccurrence(this, "RP"); + } + + /// <summary> + /// Called when an incoming positive assertion is received. + /// </summary> + /// <param name="assertion">The positive assertion.</param> + void IRelyingPartyBehavior.OnIncomingPositiveAssertion(IAuthenticationResponse assertion) { + PolicyResponse pape = assertion.GetExtension<PolicyResponse>(); + ErrorUtilities.VerifyProtocol( + pape != null && + pape.ActualPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1) && + pape.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier), + BehaviorStrings.PapeResponseOrRequiredPoliciesMissing); + + ErrorUtilities.VerifyProtocol(AllowPersonallyIdentifiableInformation || pape.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation), BehaviorStrings.PapeResponseOrRequiredPoliciesMissing); + + if (pape.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) { + ErrorUtilities.VerifyProtocol( + assertion.GetExtension<ClaimsResponse>() == null && + assertion.GetExtension<FetchResponse>() == null, + BehaviorStrings.PiiIncludedWithNoPiiPolicy); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs new file mode 100644 index 0000000..1614145 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------- +// <copyright file="CryptoKeyStoreAsRelyingPartyAssociationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Diagnostics.Contracts; + using System.Linq; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// Wraps a standard <see cref="ICryptoKeyStore"/> so that it behaves as an association store. + /// </summary> + internal class CryptoKeyStoreAsRelyingPartyAssociationStore : IRelyingPartyAssociationStore { + /// <summary> + /// The underlying key store. + /// </summary> + private readonly ICryptoKeyStore keyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="CryptoKeyStoreAsRelyingPartyAssociationStore"/> class. + /// </summary> + /// <param name="keyStore">The key store.</param> + internal CryptoKeyStoreAsRelyingPartyAssociationStore(ICryptoKeyStore keyStore) { + Requires.NotNull(keyStore, "keyStore"); + Contract.Ensures(this.keyStore == keyStore); + this.keyStore = keyStore; + } + + /// <summary> + /// Saves an <see cref="Association"/> for later recall. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="association">The association to store.</param> + public void StoreAssociation(Uri providerEndpoint, Association association) { + var cryptoKey = new CryptoKey(association.SerializePrivateData(), association.Expires); + this.keyStore.StoreKey(providerEndpoint.AbsoluteUri, association.Handle, cryptoKey); + } + + /// <summary> + /// Gets the best association (the one with the longest remaining life) for a given key. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="securityRequirements">The security requirements that the returned association must meet.</param> + /// <returns> + /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key. + /// </returns> + public Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) { + var matches = from cryptoKey in this.keyStore.GetKeys(providerEndpoint.AbsoluteUri) + where cryptoKey.Value.ExpiresUtc > DateTime.UtcNow + orderby cryptoKey.Value.ExpiresUtc descending + let assoc = Association.Deserialize(cryptoKey.Key, cryptoKey.Value.ExpiresUtc, cryptoKey.Value.Key) + where assoc.HashBitLength >= securityRequirements.MinimumHashBitLength + where assoc.HashBitLength <= securityRequirements.MaximumHashBitLength + select assoc; + return matches.FirstOrDefault(); + } + + /// <summary> + /// Gets the association for a given key and handle. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="handle">The handle of the specific association that must be recalled.</param> + /// <returns> + /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle. + /// </returns> + public Association GetAssociation(Uri providerEndpoint, string handle) { + var cryptoKey = this.keyStore.GetKey(providerEndpoint.AbsoluteUri, handle); + return cryptoKey != null ? Association.Deserialize(handle, cryptoKey.ExpiresUtc, cryptoKey.Key) : null; + } + + /// <summary> + /// Removes a specified handle that may exist in the store. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="handle">The handle of the specific association that must be deleted.</param> + /// <returns> + /// True if the association existed in this store previous to this call. + /// </returns> + public bool RemoveAssociation(Uri providerEndpoint, string handle) { + this.keyStore.RemoveKey(providerEndpoint.AbsoluteUri, handle); + return true; // return value isn't used by DNOA. + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs new file mode 100644 index 0000000..94eb5ba --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="DuplicateRequestedHostsComparer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An authentication request comparer that judges equality solely on the OP endpoint hostname. + /// </summary> + internal class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { + /// <summary> + /// The singleton instance of this comparer. + /// </summary> + private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); + + /// <summary> + /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. + /// </summary> + private DuplicateRequestedHostsComparer() { + } + + /// <summary> + /// Gets the singleton instance of this comparer. + /// </summary> + internal static IEqualityComparer<IAuthenticationRequest> Instance { + get { return instance; } + } + + #region IEqualityComparer<IAuthenticationRequest> Members + + /// <summary> + /// Determines whether the specified objects are equal. + /// </summary> + /// <param name="x">The first object to compare.</param> + /// <param name="y">The second object to compare.</param> + /// <returns> + /// true if the specified objects are equal; otherwise, false. + /// </returns> + public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + // We'll distinguish based on the host name only, which + // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, + // this multiple OP attempt thing was just a convenience feature anyway. + return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns a hash code for the specified object. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> + /// <returns>A hash code for the specified object.</returns> + /// <exception cref="T:System.ArgumentNullException"> + /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. + /// </exception> + public int GetHashCode(IAuthenticationRequest obj) { + return obj.Provider.Uri.Host.GetHashCode(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/ExtensionsInteropHelper.cs new file mode 100644 index 0000000..0f7778f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/ExtensionsInteropHelper.cs @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionsInteropHelper.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty.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> + public static class ExtensionsInteropHelper { + /// <summary> + /// Adds an Attribute Exchange (AX) extension to the authentication request + /// that asks for the same attributes as the Simple Registration (sreg) extension + /// that is already applied. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <param name="attributeFormats">The attribute formats to use in the AX request.</param> + /// <remarks> + /// <para>If discovery on the user-supplied identifier yields hints regarding which + /// extensions and attribute formats the Provider supports, this method MAY ignore the + /// <paramref name="attributeFormats"/> argument and accomodate the Provider to minimize + /// the size of the request.</para> + /// <para>If the request does not carry an sreg extension, the method logs a warning but + /// otherwise quietly returns doing nothing.</para> + /// </remarks> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")] + public static void SpreadSregToAX(this RelyingParty.IAuthenticationRequest request, AXAttributeFormats attributeFormats) { + Requires.NotNull(request, "request"); + + var req = (RelyingParty.AuthenticationRequest)request; + var sreg = req.AppliedExtensions.OfType<ClaimsRequest>().SingleOrDefault(); + if (sreg == null) { + Logger.OpenId.Debug("No Simple Registration (ClaimsRequest) extension present in the request to spread to AX."); + return; + } + + if (req.DiscoveryResult.IsExtensionSupported<ClaimsRequest>()) { + Logger.OpenId.Debug("Skipping generation of AX request because the Identifier advertises the Provider supports the Sreg extension."); + return; + } + + var ax = req.AppliedExtensions.OfType<FetchRequest>().SingleOrDefault(); + if (ax == null) { + ax = new FetchRequest(); + req.AddExtension(ax); + } + + // Try to use just one AX Type URI format if we can figure out which type the OP accepts. + AXAttributeFormats detectedFormat; + if (TryDetectOPAttributeFormat(request, out detectedFormat)) { + Logger.OpenId.Debug("Detected OP support for AX but not for Sreg. Removing Sreg extension request and using AX instead."); + attributeFormats = detectedFormat; + req.Extensions.Remove(sreg); + } else { + Logger.OpenId.Debug("Could not determine whether OP supported Sreg or AX. Using both extensions."); + } + + foreach (AXAttributeFormats format in OpenIdExtensionsInteropHelper.ForEachFormat(attributeFormats)) { + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.BirthDate.WholeBirthDate, sreg.BirthDate); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.Country, sreg.Country); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.Email, sreg.Email); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Name.FullName, sreg.FullName); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Person.Gender, sreg.Gender); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Preferences.Language, sreg.Language); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Name.Alias, sreg.Nickname); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.PostalCode, sreg.PostalCode); + OpenIdExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Preferences.TimeZone, sreg.TimeZone); + } + } + + /// <summary> + /// Looks for Simple Registration and Attribute Exchange (all known formats) + /// response extensions and returns them as a Simple Registration extension. + /// </summary> + /// <param name="response">The authentication response.</param> + /// <param name="allowUnsigned">if set to <c>true</c> unsigned extensions will be included in the search.</param> + /// <returns> + /// The Simple Registration response if found, + /// or a fabricated one based on the Attribute Exchange extension if found, + /// or just an empty <see cref="ClaimsResponse"/> if there was no data. + /// Never <c>null</c>.</returns> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")] + public static ClaimsResponse UnifyExtensionsAsSreg(this RelyingParty.IAuthenticationResponse response, bool allowUnsigned) { + Requires.NotNull(response, "response"); + + var resp = (RelyingParty.IAuthenticationResponse)response; + var sreg = allowUnsigned ? resp.GetUntrustedExtension<ClaimsResponse>() : resp.GetExtension<ClaimsResponse>(); + if (sreg != null) { + return sreg; + } + + AXAttributeFormats formats = AXAttributeFormats.All; + sreg = new ClaimsResponse(); + var fetchResponse = allowUnsigned ? resp.GetUntrustedExtension<FetchResponse>() : resp.GetExtension<FetchResponse>(); + if (fetchResponse != null) { + ((IOpenIdMessageExtension)sreg).IsSignedByRemoteParty = fetchResponse.IsSignedByProvider; + sreg.BirthDateRaw = fetchResponse.GetAttributeValue(WellKnownAttributes.BirthDate.WholeBirthDate, formats); + sreg.Country = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country, formats); + sreg.PostalCode = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.PostalCode, formats); + sreg.Email = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email, formats); + sreg.FullName = fetchResponse.GetAttributeValue(WellKnownAttributes.Name.FullName, formats); + sreg.Language = fetchResponse.GetAttributeValue(WellKnownAttributes.Preferences.Language, formats); + sreg.Nickname = fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Alias, formats); + sreg.TimeZone = fetchResponse.GetAttributeValue(WellKnownAttributes.Preferences.TimeZone, formats); + string gender = fetchResponse.GetAttributeValue(WellKnownAttributes.Person.Gender, formats); + if (gender != null) { + sreg.Gender = (Gender)OpenIdExtensionsInteropHelper.GenderEncoder.Decode(gender); + } + } + + return sreg; + } + + /// <summary> + /// Gets the attribute value if available. + /// </summary> + /// <param name="fetchResponse">The AX fetch response extension to look for the attribute value.</param> + /// <param name="typeUri">The type URI of the attribute, using the axschema.org format of <see cref="WellKnownAttributes"/>.</param> + /// <param name="formats">The AX type URI formats to search.</param> + /// <returns> + /// The first value of the attribute, if available. + /// </returns> + internal static string GetAttributeValue(this FetchResponse fetchResponse, string typeUri, AXAttributeFormats formats) { + return OpenIdExtensionsInteropHelper.ForEachFormat(formats).Select(format => fetchResponse.GetAttributeValue(OpenIdExtensionsInteropHelper.TransformAXFormat(typeUri, format))).FirstOrDefault(s => s != null); + } + + /// <summary> + /// Tries to find the exact format of AX attribute Type URI supported by the Provider. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <param name="attributeFormat">The attribute formats the RP will try if this discovery fails.</param> + /// <returns>The AX format(s) to use based on the Provider's advertised AX support.</returns> + private static bool TryDetectOPAttributeFormat(RelyingParty.IAuthenticationRequest request, out AXAttributeFormats attributeFormat) { + Requires.NotNull(request, "request"); + attributeFormat = OpenIdExtensionsInteropHelper.DetectAXFormat(request.DiscoveryResult.Capabilities); + return attributeFormat != AXAttributeFormats.None; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/UIUtilities.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/UIUtilities.cs new file mode 100644 index 0000000..4606bf7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Extensions/UIUtilities.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// <copyright file="UIUtilities.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty.Extensions.UI { + using System; + using System.Diagnostics.Contracts; + using System.Globalization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Constants used in implementing support for the UI extension. + /// </summary> + internal static class UIUtilities { + /// <summary> + /// Gets the <c>window.open</c> javascript snippet to use to open a popup window + /// compliant with the UI extension. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + /// <param name="request">The authentication request to place in the window.</param> + /// <param name="windowName">The name to assign to the popup window.</param> + /// <returns>A string starting with 'window.open' and forming just that one method call.</returns> + internal static string GetWindowPopupScript(OpenIdRelyingParty relyingParty, IAuthenticationRequest request, string windowName) { + Requires.NotNull(relyingParty, "relyingParty"); + Requires.NotNull(request, "request"); + Requires.NotNullOrEmpty(windowName, "windowName"); + + Uri popupUrl = request.RedirectingResponse.GetDirectUriRequest(relyingParty.Channel); + + return string.Format( + CultureInfo.InvariantCulture, + "(window.showModalDialog ? window.showModalDialog({0}, {1}, 'status:0;resizable:1;scroll:1;center:1;dialogWidth:{2}px; dialogHeight:{3}') : window.open({0}, {1}, 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + ((screen.width - {2}) / 2) + ',top=' + ((screen.height - {3}) / 2) + ',width={2},height={3}'));", + MessagingUtilities.GetSafeJavascriptValue(popupUrl.AbsoluteUri), + MessagingUtilities.GetSafeJavascriptValue(windowName), + OpenId.Extensions.UI.UIUtilities.PopupWidth, + OpenId.Extensions.UI.UIUtilities.PopupHeight); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs new file mode 100644 index 0000000..b9266d3 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs @@ -0,0 +1,299 @@ +//----------------------------------------------------------------------- +// <copyright file="FailedAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Wraps a failed authentication response in an <see cref="IAuthenticationResponse"/> instance + /// for public consumption by the host web site. + /// </summary> + [DebuggerDisplay("{Exception.Message}")] + internal class FailedAuthenticationResponse : IAuthenticationResponse { + /// <summary> + /// Initializes a new instance of the <see cref="FailedAuthenticationResponse"/> class. + /// </summary> + /// <param name="exception">The exception that resulted in the failed authentication.</param> + internal FailedAuthenticationResponse(Exception exception) { + Requires.NotNull(exception, "exception"); + + this.Exception = exception; + + string category = string.Empty; + if (Reporting.Enabled) { + var pe = exception as ProtocolException; + if (pe != null) { + var responseMessage = pe.FaultedMessage as IndirectSignedResponse; + if (responseMessage != null && responseMessage.ProviderEndpoint != null) { // check "required" parts because this is a failure after all + category = responseMessage.ProviderEndpoint.AbsoluteUri; + } + } + + Reporting.RecordEventOccurrence(this, category); + } + } + + #region IAuthenticationResponse Members + + /// <summary> + /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. + /// May be null for some failed authentications (i.e. failed directed identity authentications). + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This is the secure identifier that should be used for database storage and lookup. + /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects + /// user identities against spoofing and other attacks. + /// </para> + /// <para> + /// For user-friendly identifiers to display, use the + /// <see cref="FriendlyIdentifierForDisplay"/> property. + /// </para> + /// </remarks> + public Identifier ClaimedIdentifier { + get { return null; } + } + + /// <summary> + /// Gets a user-friendly OpenID Identifier for display purposes ONLY. + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before + /// sending to a browser to secure against javascript injection attacks. + /// </para> + /// <para> + /// This property retains some aspects of the user-supplied identifier that get lost + /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied + /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD). + /// For display purposes, such as text on a web page that says "You're logged in as ...", + /// this property serves to provide the =Arnott string, or whatever else is the most friendly + /// string close to what the user originally typed in. + /// </para> + /// <para> + /// If the user-supplied identifier is a URI, this property will be the URI after all + /// redirects, and with the protocol and fragment trimmed off. + /// If the user-supplied identifier is an XRI, this property will be the original XRI. + /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com), + /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI. + /// </para> + /// <para> + /// It is <b>very</b> important that this property <i>never</i> be used for database storage + /// or lookup to avoid identity spoofing and other security risks. For database storage + /// and lookup please use the <see cref="ClaimedIdentifier"/> property. + /// </para> + /// </remarks> + public string FriendlyIdentifierForDisplay { + get { return null; } + } + + /// <summary> + /// Gets the detailed success or failure status of the authentication attempt. + /// </summary> + /// <value></value> + public AuthenticationStatus Status { + get { return AuthenticationStatus.Failed; } + } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + /// <value> + /// The Provider endpoint that issued the positive assertion; + /// or <c>null</c> if information about the Provider is unavailable. + /// </value> + public IProviderEndpoint Provider { + get { return null; } + } + + /// <summary> + /// Gets the details regarding a failed authentication attempt, if available. + /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>. + /// </summary> + public Exception Exception { get; private set; } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// <para>This MAY return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public IDictionary<string, string> GetCallbackArguments() { + return EmptyDictionary<string, string>.Instance; + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public IDictionary<string, string> GetUntrustedCallbackArguments() { + return EmptyDictionary<string, string>.Instance; + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// <para>This may return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public string GetCallbackArgument(string key) { + return null; + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public string GetUntrustedCallbackArgument(string key) { + return null; + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension<T>"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetExtension<T>() where T : IOpenIdMessageExtension { + return default(T); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetExtension(Type extensionType) { + return null; + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response, without + /// requiring it to be signed by the Provider. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension<T>"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension { + return default(T); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) { + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs new file mode 100644 index 0000000..52e828c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------- +// <copyright file="IRelyingPartyAssociationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Diagnostics.Contracts; + + /// <summary> + /// Stores <see cref="Association"/>s for lookup by their handle, keeping + /// associations separated by a given OP Endpoint. + /// </summary> + /// <remarks> + /// Expired associations should be periodically cleared out of an association store. + /// This should be done frequently enough to avoid a memory leak, but sparingly enough + /// to not be a performance drain. Because this balance can vary by host, it is the + /// responsibility of the host to initiate this cleaning. + /// </remarks> + [ContractClass(typeof(IRelyingPartyAssociationStoreContract))] + public interface IRelyingPartyAssociationStore { + /// <summary> + /// Saves an <see cref="Association"/> for later recall. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="association">The association to store.</param> + /// <remarks> + /// If the new association conflicts (in OP endpoint and association handle) with an existing association, + /// (which should never happen by the way) implementations may overwrite the previously saved association. + /// </remarks> + void StoreAssociation(Uri providerEndpoint, Association association); + + /// <summary> + /// Gets the best association (the one with the longest remaining life) for a given key. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="securityRequirements">The security requirements that the returned association must meet.</param> + /// <returns> + /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key. + /// </returns> + /// <remarks> + /// In the event that multiple associations exist for the given + /// <paramref name="providerEndpoint"/>, it is important for the + /// implementation for this method to use the <paramref name="securityRequirements"/> + /// to pick the best (highest grade or longest living as the host's policy may dictate) + /// association that fits the security requirements. + /// Associations that are returned that do not meet the security requirements will be + /// ignored and a new association created. + /// </remarks> + Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements); + + /// <summary> + /// Gets the association for a given key and handle. + /// </summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="handle">The handle of the specific association that must be recalled.</param> + /// <returns>The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.</returns> + Association GetAssociation(Uri providerEndpoint, string handle); + + /// <summary>Removes a specified handle that may exist in the store.</summary> + /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> + /// <param name="handle">The handle of the specific association that must be deleted.</param> + /// <returns> + /// Deprecated. The return value is insignificant. + /// Previously: True if the association existed in this store previous to this call. + /// </returns> + /// <remarks> + /// No exception should be thrown if the association does not exist in the store + /// before this call. + /// </remarks> + bool RemoveAssociation(Uri providerEndpoint, string handle); + } + + /// <summary> + /// Code Contract for the <see cref="IRelyingPartyAssociationStore"/> class. + /// </summary> + [ContractClassFor(typeof(IRelyingPartyAssociationStore))] + internal abstract class IRelyingPartyAssociationStoreContract : IRelyingPartyAssociationStore { + #region IAssociationStore Members + + /// <summary> + /// Saves an <see cref="Association"/> for later recall. + /// </summary> + /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for providers).</param> + /// <param name="association">The association to store.</param> + /// <remarks> + /// TODO: what should implementations do on association handle conflict? + /// </remarks> + void IRelyingPartyAssociationStore.StoreAssociation(Uri providerEndpoint, Association association) { + Requires.NotNull(providerEndpoint, "providerEndpoint"); + Requires.NotNull(association, "association"); + throw new NotImplementedException(); + } + + /// <summary> + /// Gets the best association (the one with the longest remaining life) for a given key. + /// </summary> + /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <param name="securityRequirements">The security requirements that the returned association must meet.</param> + /// <returns> + /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key. + /// </returns> + /// <remarks> + /// In the event that multiple associations exist for the given + /// <paramref name="providerEndpoint"/>, it is important for the + /// implementation for this method to use the <paramref name="securityRequirements"/> + /// to pick the best (highest grade or longest living as the host's policy may dictate) + /// association that fits the security requirements. + /// Associations that are returned that do not meet the security requirements will be + /// ignored and a new association created. + /// </remarks> + Association IRelyingPartyAssociationStore.GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) { + Requires.NotNull(providerEndpoint, "providerEndpoint"); + Requires.NotNull(securityRequirements, "securityRequirements"); + throw new NotImplementedException(); + } + + /// <summary> + /// Gets the association for a given key and handle. + /// </summary> + /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <param name="handle">The handle of the specific association that must be recalled.</param> + /// <returns> + /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle. + /// </returns> + Association IRelyingPartyAssociationStore.GetAssociation(Uri providerEndpoint, string handle) { + Requires.NotNull(providerEndpoint, "providerEndpoint"); + Contract.Requires(!String.IsNullOrEmpty(handle)); + throw new NotImplementedException(); + } + + /// <summary> + /// Removes a specified handle that may exist in the store. + /// </summary> + /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <param name="handle">The handle of the specific association that must be deleted.</param> + /// <returns> + /// True if the association existed in this store previous to this call. + /// </returns> + /// <remarks> + /// No exception should be thrown if the association does not exist in the store + /// before this call. + /// </remarks> + bool IRelyingPartyAssociationStore.RemoveAssociation(Uri providerEndpoint, string handle) { + Requires.NotNull(providerEndpoint, "providerEndpoint"); + Contract.Requires(!String.IsNullOrEmpty(handle)); + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs new file mode 100644 index 0000000..2942c9d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// <copyright file="ISetupRequiredAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Diagnostics.Contracts; + + /// <summary> + /// An interface to expose useful properties and functionality for handling + /// authentication responses that are returned from Immediate authentication + /// requests that require a subsequent request to be made in non-immediate mode. + /// </summary> + [ContractClass(typeof(ISetupRequiredAuthenticationResponseContract))] + public interface ISetupRequiredAuthenticationResponse { + /// <summary> + /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/> + /// in a subsequent authentication attempt. + /// </summary> + Identifier UserSuppliedIdentifier { get; } + } + + /// <summary> + /// Code contract class for the <see cref="ISetupRequiredAuthenticationResponse"/> type. + /// </summary> + [ContractClassFor(typeof(ISetupRequiredAuthenticationResponse))] + internal abstract class ISetupRequiredAuthenticationResponseContract : ISetupRequiredAuthenticationResponse { + /// <summary> + /// Initializes a new instance of the <see cref="ISetupRequiredAuthenticationResponseContract"/> class. + /// </summary> + protected ISetupRequiredAuthenticationResponseContract() { + } + + #region ISetupRequiredAuthenticationResponse Members + + /// <summary> + /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/> + /// in a subsequent authentication attempt. + /// </summary> + Identifier ISetupRequiredAuthenticationResponse.UserSuppliedIdentifier { + get { + Requires.ValidState(((IAuthenticationResponse)this).Status == AuthenticationStatus.SetupRequired, OpenIdStrings.OperationOnlyValidForSetupRequiredState); + throw new System.NotImplementedException(); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs new file mode 100644 index 0000000..4b21fea --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs @@ -0,0 +1,313 @@ +//----------------------------------------------------------------------- +// <copyright file="NegativeAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Wraps a negative assertion response in an <see cref="IAuthenticationResponse"/> instance + /// for public consumption by the host web site. + /// </summary> + internal class NegativeAuthenticationResponse : IAuthenticationResponse, ISetupRequiredAuthenticationResponse { + /// <summary> + /// The negative assertion message that was received by the RP that was used + /// to create this instance. + /// </summary> + private readonly NegativeAssertionResponse response; + + /// <summary> + /// Initializes a new instance of the <see cref="NegativeAuthenticationResponse"/> class. + /// </summary> + /// <param name="response">The negative assertion response received by the Relying Party.</param> + internal NegativeAuthenticationResponse(NegativeAssertionResponse response) { + Requires.NotNull(response, "response"); + this.response = response; + + Reporting.RecordEventOccurrence(this, string.Empty); + } + + #region IAuthenticationResponse Properties + + /// <summary> + /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. + /// May be null for some failed authentications (i.e. failed directed identity authentications). + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This is the secure identifier that should be used for database storage and lookup. + /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects + /// user identities against spoofing and other attacks. + /// </para> + /// <para> + /// For user-friendly identifiers to display, use the + /// <see cref="FriendlyIdentifierForDisplay"/> property. + /// </para> + /// </remarks> + public Identifier ClaimedIdentifier { + get { return null; } + } + + /// <summary> + /// Gets a user-friendly OpenID Identifier for display purposes ONLY. + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before + /// sending to a browser to secure against javascript injection attacks. + /// </para> + /// <para> + /// This property retains some aspects of the user-supplied identifier that get lost + /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied + /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD). + /// For display purposes, such as text on a web page that says "You're logged in as ...", + /// this property serves to provide the =Arnott string, or whatever else is the most friendly + /// string close to what the user originally typed in. + /// </para> + /// <para> + /// If the user-supplied identifier is a URI, this property will be the URI after all + /// redirects, and with the protocol and fragment trimmed off. + /// If the user-supplied identifier is an XRI, this property will be the original XRI. + /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com), + /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI. + /// </para> + /// <para> + /// It is <b>very</b> important that this property <i>never</i> be used for database storage + /// or lookup to avoid identity spoofing and other security risks. For database storage + /// and lookup please use the <see cref="ClaimedIdentifier"/> property. + /// </para> + /// </remarks> + public string FriendlyIdentifierForDisplay { + get { return null; } + } + + /// <summary> + /// Gets the detailed success or failure status of the authentication attempt. + /// </summary> + /// <value></value> + public AuthenticationStatus Status { + get { return this.response.Immediate ? AuthenticationStatus.SetupRequired : AuthenticationStatus.Canceled; } + } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + /// <value> + /// The Provider endpoint that issued the positive assertion; + /// or <c>null</c> if information about the Provider is unavailable. + /// </value> + public IProviderEndpoint Provider { + get { return null; } + } + + /// <summary> + /// Gets the details regarding a failed authentication attempt, if available. + /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>. + /// </summary> + /// <value></value> + public Exception Exception { + get { return null; } + } + + #endregion + + #region ISetupRequiredAuthenticationResponse Members + + /// <summary> + /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/> + /// in a subsequent authentication attempt. + /// </summary> + /// <value></value> + public Identifier UserSuppliedIdentifier { + get { + ErrorUtilities.VerifyOperation(((IAuthenticationResponse)this).Status == AuthenticationStatus.SetupRequired, OpenIdStrings.OperationOnlyValidForSetupRequiredState); + string userSuppliedIdentifier; + this.response.ExtraData.TryGetValue(AuthenticationRequest.UserSuppliedIdentifierParameterName, out userSuppliedIdentifier); + return userSuppliedIdentifier; + } + } + + #endregion + + #region IAuthenticationResponse Methods + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// <para>This may return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public string GetCallbackArgument(string key) { + return null; + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public string GetUntrustedCallbackArgument(string key) { + return null; + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// <para>This MAY return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public IDictionary<string, string> GetCallbackArguments() { + return EmptyDictionary<string, string>.Instance; + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public IDictionary<string, string> GetUntrustedCallbackArguments() { + return EmptyDictionary<string, string>.Instance; + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension<T>"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetExtension<T>() where T : IOpenIdMessageExtension { + return default(T); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetExtension(Type extensionType) { + return null; + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response, without + /// requiring it to be signed by the Provider. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension<T>"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension { + return this.response.Extensions.OfType<T>().FirstOrDefault(); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) { + return this.response.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs new file mode 100644 index 0000000..509e319 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -0,0 +1,903 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + 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.Globalization; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A delegate that decides whether a given OpenID Provider endpoint may be + /// considered for authenticating a user. + /// </summary> + /// <param name="endpoint">The endpoint for consideration.</param> + /// <returns> + /// <c>True</c> if the endpoint should be considered. + /// <c>False</c> to remove it from the pool of acceptable providers. + /// </returns> + public delegate bool EndpointSelector(IProviderEndpoint endpoint); + + /// <summary> + /// Provides the programmatic facilities to act as an OpenID relying party. + /// </summary> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")] + [ContractVerification(true)] + public class OpenIdRelyingParty : IDisposable { + /// <summary> + /// The name of the key to use in the HttpApplication cache to store the + /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use. + /// </summary> + private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.HttpApplicationStore"; + + /// <summary> + /// Backing store for the <see cref="Behaviors"/> property. + /// </summary> + private readonly ObservableCollection<IRelyingPartyBehavior> behaviors = new ObservableCollection<IRelyingPartyBehavior>(); + + /// <summary> + /// Backing field for the <see cref="DiscoveryServices"/> property. + /// </summary> + private readonly IList<IIdentifierDiscoveryService> discoveryServices = new List<IIdentifierDiscoveryService>(2); + + /// <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="NonVerifyingRelyingParty"/> property. + /// </summary> + private OpenIdRelyingParty nonVerifyingRelyingParty; + + /// <summary> + /// The lock to obtain when initializing the <see cref="nonVerifyingRelyingParty"/> member. + /// </summary> + private object nonVerifyingRelyingPartyInitLock = new object(); + + /// <summary> + /// A dictionary of extension response types and the javascript member + /// name to map them to on the user agent. + /// </summary> + private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); + + /// <summary> + /// Backing field for the <see cref="SecuritySettings"/> property. + /// </summary> + private RelyingPartySecuritySettings securitySettings; + + /// <summary> + /// Backing store for the <see cref="EndpointOrder"/> property. + /// </summary> + private Comparison<IdentifierDiscoveryResult> endpointOrder = DefaultEndpointOrder; + + /// <summary> + /// Backing field for the <see cref="Channel"/> property. + /// </summary> + private Channel channel; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. + /// </summary> + public OpenIdRelyingParty() + : this(OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. + /// </summary> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> + public OpenIdRelyingParty(IOpenIdApplicationStore applicationStore) + : this(applicationStore, applicationStore) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The association store. If null, the relying party will always operate in "dumb mode".</param> + /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")] + private OpenIdRelyingParty(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore) { + // If we are a smart-mode RP (supporting associations), then we MUST also be + // capable of storing nonces to prevent replay attacks. + // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays. + Requires.True(cryptoKeyStore == null || nonceStore != null, null, OpenIdStrings.AssociationStoreRequiresNonceStore); + + this.securitySettings = OpenIdElement.Configuration.RelyingParty.SecuritySettings.CreateSecuritySettings(); + + foreach (var discoveryService in OpenIdElement.Configuration.RelyingParty.DiscoveryServices.CreateInstances(true)) { + this.discoveryServices.Add(discoveryService); + } + + this.behaviors.CollectionChanged += this.OnBehaviorsChanged; + foreach (var behavior in OpenIdElement.Configuration.RelyingParty.Behaviors.CreateInstances(false)) { + this.behaviors.Add(behavior); + } + + // Without a nonce store, we must rely on the Provider to protect against + // replay attacks. But only 2.0+ Providers can be expected to provide + // replay protection. + if (nonceStore == null && + this.SecuritySettings.ProtectDownlevelReplayAttacks && + this.SecuritySettings.MinimumRequiredOpenIdVersion < ProtocolVersion.V20) { + Logger.OpenId.Warn("Raising minimum OpenID version requirement for Providers to 2.0 to protect this stateless RP from replay attacks."); + this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; + } + + if (cryptoKeyStore == null) { + cryptoKeyStore = new MemoryCryptoKeyStore(); + } + + this.channel = new OpenIdRelyingPartyChannel(cryptoKeyStore, nonceStore, this.SecuritySettings); + this.AssociationManager = new AssociationManager(this.Channel, new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore), this.SecuritySettings); + + Reporting.RecordFeatureAndDependencyUse(this, cryptoKeyStore, nonceStore); + } + + /// <summary> + /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority + /// attribute to determine order. + /// </summary> + /// <remarks> + /// Endpoints lacking any priority value are sorted to the end of the list. + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static Comparison<IdentifierDiscoveryResult> DefaultEndpointOrder { + get { return IdentifierDiscoveryResult.EndpointOrder; } + } + + /// <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 { + Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null); + + HttpContext context = HttpContext.Current; + ErrorUtilities.VerifyOperation(context != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(IOpenIdApplicationStore).Name); + 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 StandardRelyingPartyApplicationStore(); + } + } finally { + context.Application.UnLock(); + } + } + + return store; + } + } + + /// <summary> + /// Gets or sets the channel to use for sending/receiving messages. + /// </summary> + public Channel Channel { + get { + return this.channel; + } + + set { + Requires.NotNull(value, "value"); + this.channel = value; + this.AssociationManager.Channel = value; + } + } + + /// <summary> + /// Gets the security settings used by this Relying Party. + /// </summary> + public RelyingPartySecuritySettings SecuritySettings { + get { + Contract.Ensures(Contract.Result<RelyingPartySecuritySettings>() != null); + return this.securitySettings; + } + + internal set { + Requires.NotNull(value, "value"); + this.securitySettings = value; + this.AssociationManager.SecuritySettings = value; + } + } + + /// <summary> + /// Gets or sets the optional Provider Endpoint filter to use. + /// </summary> + /// <remarks> + /// Provides a way to optionally filter the providers that may be used in authenticating a user. + /// If provided, the delegate should return true to accept an endpoint, and false to reject it. + /// If null, all identity providers will be accepted. This is the default. + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public EndpointSelector EndpointFilter { get; set; } + + /// <summary> + /// Gets or sets the ordering routine that will determine which XRDS + /// Service element to try first + /// </summary> + /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value> + /// <remarks> + /// This may never be null. To reset to default behavior this property + /// can be set to the value of <see cref="DefaultEndpointOrder"/>. + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Comparison<IdentifierDiscoveryResult> EndpointOrder { + get { + return this.endpointOrder; + } + + set { + Requires.NotNull(value, "value"); + this.endpointOrder = value; + } + } + + /// <summary> + /// Gets the extension factories. + /// </summary> + public IList<IOpenIdExtensionFactory> ExtensionFactories { + get { return this.Channel.GetExtensionFactories(); } + } + + /// <summary> + /// Gets a list of custom behaviors to apply to OpenID actions. + /// </summary> + /// <remarks> + /// Adding behaviors can impact the security settings of this <see cref="OpenIdRelyingParty"/> + /// instance in ways that subsequently removing the behaviors will not reverse. + /// </remarks> + public ICollection<IRelyingPartyBehavior> Behaviors { + get { return this.behaviors; } + } + + /// <summary> + /// Gets the list of services that can perform discovery on identifiers given to this relying party. + /// </summary> + public IList<IIdentifierDiscoveryService> DiscoveryServices { + get { return this.discoveryServices; } + } + + /// <summary> + /// Gets a value indicating whether this Relying Party can sign its return_to + /// parameter in outgoing authentication requests. + /// </summary> + internal bool CanSignCallbackArguments { + get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); } + } + + /// <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 association manager. + /// </summary> + internal AssociationManager AssociationManager { get; private set; } + + /// <summary> + /// Gets the <see cref="OpenIdRelyingParty"/> instance used to process authentication responses + /// without verifying the assertion or consuming nonces. + /// </summary> + protected OpenIdRelyingParty NonVerifyingRelyingParty { + get { + if (this.nonVerifyingRelyingParty == null) { + lock (this.nonVerifyingRelyingPartyInitLock) { + if (this.nonVerifyingRelyingParty == null) { + this.nonVerifyingRelyingParty = OpenIdRelyingParty.CreateNonVerifying(); + } + } + } + + return this.nonVerifyingRelyingParty; + } + } + + /// <summary> + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <param name="realm"> + /// The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/. + /// </param> + /// <param name="returnToUrl"> + /// The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider. + /// </param> + /// <returns> + /// An authentication request object to customize the request and generate + /// an object to send to the user agent to initiate the authentication. + /// </returns> + /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception> + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.NotNull(realm, "realm"); + Requires.NotNull(returnToUrl, "returnToUrl"); + Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null); + try { + return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First(); + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// <summary> + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <param name="realm"> + /// The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/. + /// </param> + /// <returns> + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// </returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception> + /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception> + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.NotNull(realm, "realm"); + Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null); + try { + var result = this.CreateRequests(userSuppliedIdentifier, realm).First(); + Contract.Assume(result != null); + return result; + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// <summary> + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <returns> + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// </returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception> + /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception> + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null); + try { + return this.CreateRequests(userSuppliedIdentifier).First(); + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// <summary> + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <param name="realm"> + /// The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/. + /// </param> + /// <param name="returnToUrl"> + /// The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider. + /// </param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// <para>Any individual generated request can satisfy the authentication. + /// The generated requests are sorted in preferred order. + /// Each request is generated as it is enumerated to. Associations are created only as + /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para> + /// <para>No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead.</para> + /// </remarks> + public virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.NotNull(realm, "realm"); + Requires.NotNull(returnToUrl, "returnToUrl"); + Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); + + return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults(); + } + + /// <summary> + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <param name="realm"> + /// The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/. + /// </param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// <para>Any individual generated request can satisfy the authentication. + /// The generated requests are sorted in preferred order. + /// Each request is generated as it is enumerated to. Associations are created only as + /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para> + /// <para>No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead.</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> + public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) { + Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.NotNull(realm, "realm"); + Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); + + // This next code contract is a BAD idea, because it causes each authentication request to be generated + // at least an extra time. + ////Contract.Ensures(Contract.ForAll(Contract.Result<IEnumerable<IAuthenticationRequest>>(), el => el != null)); + + // Build the return_to URL + UriBuilder returnTo = new UriBuilder(this.Channel.GetRequestFromContext().UrlBeforeRewriting); + + // Trim off any parameters with an "openid." prefix, and a few known others + // to avoid carrying state from a prior login attempt. + returnTo.Query = string.Empty; + NameValueCollection queryParams = this.Channel.GetRequestFromContext().QueryStringBeforeRewriting; + var returnToParams = new Dictionary<string, string>(queryParams.Count); + foreach (string key in queryParams) { + if (!IsOpenIdSupportingParameter(key) && key != null) { + returnToParams.Add(key, queryParams[key]); + } + } + returnTo.AppendQueryArgs(returnToParams); + + return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri); + } + + /// <summary> + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier"> + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// </param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// <para>Any individual generated request can satisfy the authentication. + /// The generated requests are sorted in preferred order. + /// Each request is generated as it is enumerated to. Associations are created only as + /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para> + /// <para>No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead.</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> + public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) { + Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); + + return this.CreateRequests(userSuppliedIdentifier, Realm.AutoDetect); + } + + /// <summary> + /// Gets an authentication response from a Provider. + /// </summary> + /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + public IAuthenticationResponse GetResponse() { + Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + return this.GetResponse(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Gets an authentication response from a Provider. + /// </summary> + /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param> + /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns> + public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) { + Requires.NotNull(httpRequestInfo, "httpRequestInfo"); + try { + var message = this.Channel.ReadFromRequest(httpRequestInfo); + PositiveAssertionResponse positiveAssertion; + NegativeAssertionResponse negativeAssertion; + IndirectSignedResponse positiveExtensionOnly; + if ((positiveAssertion = message as PositiveAssertionResponse) != null) { + // We need to make sure that this assertion is coming from an endpoint + // that the host deems acceptable. + var providerEndpoint = new SimpleXrdsProviderEndpoint(positiveAssertion); + ErrorUtilities.VerifyProtocol( + this.FilterEndpoint(providerEndpoint), + OpenIdStrings.PositiveAssertionFromNonQualifiedProvider, + providerEndpoint.Uri); + + var response = new PositiveAuthenticationResponse(positiveAssertion, this); + foreach (var behavior in this.Behaviors) { + behavior.OnIncomingPositiveAssertion(response); + } + + return response; + } else if ((positiveExtensionOnly = message as IndirectSignedResponse) != null) { + return new PositiveAnonymousResponse(positiveExtensionOnly); + } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) { + return new NegativeAuthenticationResponse(negativeAssertion); + } else if (message != null) { + Logger.OpenId.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name); + } + + return null; + } catch (ProtocolException ex) { + return new FailedAuthenticationResponse(ex); + } + } + + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <returns>The HTTP response to send to this HTTP request.</returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + public OutgoingWebResponse ProcessResponseFromPopup() { + Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <returns>The HTTP response to send to this HTTP request.</returns> + public OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request) { + Requires.NotNull(request, "request"); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(request, null); + } + + /// <summary> + /// Allows an OpenID extension to read data out of an unverified positive authentication assertion + /// and send it down to the client browser so that Javascript running on the page can perform + /// some preprocessing on the extension data. + /// </summary> + /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam> + /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param> + /// <remarks> + /// This method should be called before <see cref="ProcessResponseFromPopup()"/>. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] + public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { + Requires.NotNullOrEmpty(propertyName, "propertyName"); + ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); + foreach (var ext in this.clientScriptExtensions.Keys) { + ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); + } + this.clientScriptExtensions.Add(typeof(T), propertyName); + } + + #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); + } + + #endregion + + /// <summary> + /// Determines whether some parameter name belongs to OpenID or this library + /// as a protocol or internal parameter name. + /// </summary> + /// <param name="parameterName">Name of the parameter.</param> + /// <returns> + /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>. + /// </returns> + internal static bool IsOpenIdSupportingParameter(string parameterName) { + // Yes, it is possible with some query strings to have a null or empty parameter name + if (string.IsNullOrEmpty(parameterName)) { + return false; + } + + Protocol protocol = Protocol.Default; + return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase) + || parameterName.StartsWith(OpenIdUtilities.CustomParameterPrefix, StringComparison.Ordinal); + } + + /// <summary> + /// Creates a relying party that does not verify incoming messages against + /// nonce or association stores. + /// </summary> + /// <returns>The instantiated <see cref="OpenIdRelyingParty"/>.</returns> + /// <remarks> + /// Useful for previewing messages while + /// allowing them to be fully processed and verified later. + /// </remarks> + internal static OpenIdRelyingParty CreateNonVerifying() { + OpenIdRelyingParty rp = new OpenIdRelyingParty(); + try { + rp.Channel = OpenIdRelyingPartyChannel.CreateNonVerifyingChannel(); + return rp; + } catch { + rp.Dispose(); + throw; + } + } + + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <param name="callback">The callback fired after the response status has been determined but before the Javascript response is formulated.</param> + /// <returns> + /// The HTTP response to send to this HTTP request. + /// </returns> + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpenID", Justification = "real word"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "iframe", Justification = "Code contracts")] + internal OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request, Action<AuthenticationStatus> callback) { + Requires.NotNull(request, "request"); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + string extensionsJson = null; + var authResponse = this.NonVerifyingRelyingParty.GetResponse(); + ErrorUtilities.VerifyProtocol(authResponse != null, OpenIdStrings.PopupRedirectMissingResponse); + + // Give the caller a chance to notify the hosting page and fill up the clientScriptExtensions collection. + if (callback != null) { + callback(authResponse.Status); + } + + Logger.OpenId.DebugFormat("Popup or iframe callback from OP: {0}", request.Url); + Logger.Controls.DebugFormat( + "An authentication response was found in a popup window or iframe using a non-verifying RP with status: {0}", + authResponse.Status); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + var extensionsDictionary = new Dictionary<string, string>(); + foreach (var pair in this.clientScriptExtensions) { + IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); + if (extension == null) { + continue; + } + var positiveResponse = (PositiveAuthenticationResponse)authResponse; + string js = extension.InitializeJavaScriptData(positiveResponse.Response); + if (!string.IsNullOrEmpty(js)) { + extensionsDictionary[pair.Value] = js; + } + } + + extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); + } + + string payload = "document.URL"; + if (request.HttpMethod == "POST") { + // Promote all form variables to the query string, but since it won't be passed + // to any server (this is a javascript window-to-window transfer) the length of + // it can be arbitrarily long, whereas it was POSTed here probably because it + // was too long for HTTP transit. + UriBuilder payloadUri = new UriBuilder(request.Url); + payloadUri.AppendQueryArgs(request.Form.ToDictionary()); + payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); + } + + if (!string.IsNullOrEmpty(extensionsJson)) { + payload += ", " + extensionsJson; + } + + return InvokeParentPageScript("dnoa_internal.processAuthorizationResult(" + payload + ")"); + } + + /// <summary> + /// Performs discovery on the specified identifier. + /// </summary> + /// <param name="identifier">The identifier to discover services for.</param> + /// <returns>A non-null sequence of services discovered for the identifier.</returns> + internal IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier) { + Requires.NotNull(identifier, "identifier"); + Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null); + + IEnumerable<IdentifierDiscoveryResult> results = Enumerable.Empty<IdentifierDiscoveryResult>(); + foreach (var discoverer in this.DiscoveryServices) { + bool abortDiscoveryChain; + var discoveryResults = discoverer.Discover(identifier, this.WebRequestHandler, out abortDiscoveryChain).CacheGeneratedResults(); + results = results.Concat(discoveryResults); + if (abortDiscoveryChain) { + Logger.OpenId.InfoFormat("Further discovery on '{0}' was stopped by the {1} discovery service.", identifier, discoverer.GetType().Name); + break; + } + } + + // If any OP Identifier service elements were found, we must not proceed + // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2. + // For a discussion on this topic, see + // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8 + // Sometimes the IIdentifierDiscoveryService will automatically filter this for us, but + // just to be sure, we'll do it here as well. + if (!this.SecuritySettings.AllowDualPurposeIdentifiers) { + results = results.CacheGeneratedResults(); // avoid performing discovery repeatedly + var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier); + var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier); + results = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers; + } + + return results; + } + + /// <summary> + /// Checks whether a given OP Endpoint is permitted by the host relying party. + /// </summary> + /// <param name="endpoint">The OP endpoint.</param> + /// <returns><c>true</c> if the OP Endpoint is allowed; <c>false</c> otherwise.</returns> + protected internal bool FilterEndpoint(IProviderEndpoint endpoint) { + if (this.SecuritySettings.RejectAssertionsFromUntrustedProviders) { + if (!this.SecuritySettings.TrustedProviderEndpoints.Contains(endpoint.Uri)) { + Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because it is not on the exclusive trusted provider whitelist.", endpoint.Uri.AbsoluteUri); + return false; + } + } + + if (endpoint.Version < Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version) { + Logger.OpenId.InfoFormat( + "Filtering out OP endpoint {0} because it implements OpenID {1} but this relying party requires OpenID {2} or later.", + endpoint.Uri.AbsoluteUri, + endpoint.Version, + Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version); + return false; + } + + if (this.EndpointFilter != null) { + if (!this.EndpointFilter(endpoint)) { + Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because the host rejected it.", endpoint.Uri.AbsoluteUri); + return false; + } + } + + return true; + } + + /// <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> + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (this.nonVerifyingRelyingParty != null) { + this.nonVerifyingRelyingParty.Dispose(); + this.nonVerifyingRelyingParty = null; + } + + // Tear off the instance member as a local variable for thread safety. + IDisposable disposableChannel = this.channel as IDisposable; + if (disposableChannel != null) { + disposableChannel.Dispose(); + } + } + } + + /// <summary> + /// Invokes a method on a parent frame or window and closes the calling popup window if applicable. + /// </summary> + /// <param name="methodCall">The method to call on the parent window, including + /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> + /// <returns>The entire HTTP response to send to the popup window or iframe to perform the invocation.</returns> + private static OutgoingWebResponse InvokeParentPageScript(string methodCall) { + Requires.NotNullOrEmpty(methodCall, "methodCall"); + + Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("<html><body><script type='text/javascript' language='javascript'><!--"); + builder.AppendLine("//<![CDATA["); + builder.Append(@" var inPopup = !window.frameElement; + var objSrc = inPopup ? window.opener : window.frameElement; +"); + + // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, + // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already + // whether to call window.self.close() after the call. + string htmlFormat = @" if (inPopup) {{ + try {{ + objSrc.{0}; + }} catch (ex) {{ + alert(ex); + }} finally {{ + window.self.close(); + }} + }} else {{ + objSrc.{0}; + }}"; + builder.AppendFormat(CultureInfo.InvariantCulture, htmlFormat, methodCall); + builder.AppendLine("//]]>--></script>"); + builder.AppendLine("</body></html>"); + + var response = new OutgoingWebResponse(); + response.Body = builder.ToString(); + response.Headers.Add(HttpResponseHeader.ContentType, new ContentType("text/html").ToString()); + return response; + } + + /// <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 (IRelyingPartyBehavior profile in e.NewItems) { + profile.ApplySecuritySettings(this.SecuritySettings); + Reporting.RecordFeatureUse(profile); + } + } + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.SecuritySettings != null); + Contract.Invariant(this.Channel != null); + Contract.Invariant(this.EndpointOrder != null); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs new file mode 100644 index 0000000..de82bed --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs @@ -0,0 +1,347 @@ +//----------------------------------------------------------------------- +// <copyright file="PositiveAnonymousResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Wraps an extension-only response from the OP in an <see cref="IAuthenticationResponse"/> instance + /// for public consumption by the host web site. + /// </summary> + internal class PositiveAnonymousResponse : IAuthenticationResponse { + /// <summary> + /// Backin field for the <see cref="Response"/> property. + /// </summary> + private readonly IndirectSignedResponse response; + + /// <summary> + /// Information about the OP endpoint that issued this assertion. + /// </summary> + private readonly IProviderEndpoint provider; + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAnonymousResponse"/> class. + /// </summary> + /// <param name="response">The response message.</param> + protected internal PositiveAnonymousResponse(IndirectSignedResponse response) { + Requires.NotNull(response, "response"); + + this.response = response; + if (response.ProviderEndpoint != null && response.Version != null) { + this.provider = new ProviderEndpointDescription(response.ProviderEndpoint, response.Version); + } + + // Derived types of this are responsible to log an appropriate message for themselves. + if (Logger.OpenId.IsInfoEnabled && this.GetType() == typeof(PositiveAnonymousResponse)) { + Logger.OpenId.Info("Received anonymous (identity-less) positive assertion."); + } + + if (response.ProviderEndpoint != null) { + Reporting.RecordEventOccurrence(this, response.ProviderEndpoint.AbsoluteUri); + } + } + + #region IAuthenticationResponse Properties + + /// <summary> + /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. + /// May be null for some failed authentications (i.e. failed directed identity authentications). + /// </summary> + /// <remarks> + /// <para> + /// This is the secure identifier that should be used for database storage and lookup. + /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects + /// user identities against spoofing and other attacks. + /// </para> + /// <para> + /// For user-friendly identifiers to display, use the + /// <see cref="FriendlyIdentifierForDisplay"/> property. + /// </para> + /// </remarks> + public virtual Identifier ClaimedIdentifier { + get { return null; } + } + + /// <summary> + /// Gets a user-friendly OpenID Identifier for display purposes ONLY. + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before + /// sending to a browser to secure against javascript injection attacks. + /// </para> + /// <para> + /// This property retains some aspects of the user-supplied identifier that get lost + /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied + /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD). + /// For display purposes, such as text on a web page that says "You're logged in as ...", + /// this property serves to provide the =Arnott string, or whatever else is the most friendly + /// string close to what the user originally typed in. + /// </para> + /// <para> + /// If the user-supplied identifier is a URI, this property will be the URI after all + /// redirects, and with the protocol and fragment trimmed off. + /// If the user-supplied identifier is an XRI, this property will be the original XRI. + /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com), + /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI. + /// </para> + /// <para> + /// It is <b>very</b> important that this property <i>never</i> be used for database storage + /// or lookup to avoid identity spoofing and other security risks. For database storage + /// and lookup please use the <see cref="ClaimedIdentifier"/> property. + /// </para> + /// </remarks> + public virtual string FriendlyIdentifierForDisplay { + get { return null; } + } + + /// <summary> + /// Gets the detailed success or failure status of the authentication attempt. + /// </summary> + public virtual AuthenticationStatus Status { + get { return AuthenticationStatus.ExtensionsOnly; } + } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + /// <value> + /// The Provider endpoint that issued the positive assertion; + /// or <c>null</c> if information about the Provider is unavailable. + /// </value> + public IProviderEndpoint Provider { + get { return this.provider; } + } + + /// <summary> + /// Gets the details regarding a failed authentication attempt, if available. + /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>. + /// </summary> + /// <value></value> + public Exception Exception { + get { return null; } + } + + #endregion + + /// <summary> + /// Gets a value indicating whether trusted callback arguments are available. + /// </summary> + /// <remarks> + /// We use this internally to avoid logging a warning during a standard snapshot creation. + /// </remarks> + internal bool TrustedCallbackArgumentsAvailable { + get { return this.response.ReturnToParametersSignatureValidated; } + } + + /// <summary> + /// Gets the positive extension-only message the Relying Party received that this instance wraps. + /// </summary> + protected internal IndirectSignedResponse Response { + get { return this.response; } + } + + #region IAuthenticationResponse methods + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// Callback parameters are only available if they are complete and untampered with + /// since the original request message (as proven by a signature). + /// If the relying party is operating in stateless mode <c>null</c> is always + /// returned since the callback arguments could not be signed to protect against + /// tampering. + /// </remarks> + public string GetCallbackArgument(string key) { + if (this.response.ReturnToParametersSignatureValidated) { + return this.GetUntrustedCallbackArgument(key); + } else { + Logger.OpenId.WarnFormat(OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + return null; + } + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public string GetUntrustedCallbackArgument(string key) { + return this.response.GetReturnToArgument(key); + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// Callback parameters are only available if they are complete and untampered with + /// since the original request message (as proven by a signature). + /// If the relying party is operating in stateless mode an empty dictionary is always + /// returned since the callback arguments could not be signed to protect against + /// tampering. + /// </remarks> + public IDictionary<string, string> GetCallbackArguments() { + if (this.response.ReturnToParametersSignatureValidated) { + return this.GetUntrustedCallbackArguments(); + } else { + Logger.OpenId.WarnFormat(OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + return EmptyDictionary<string, string>.Instance; + } + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// Callback parameters are only available if they are complete and untampered with + /// since the original request message (as proven by a signature). + /// If the relying party is operating in stateless mode an empty dictionary is always + /// returned since the callback arguments could not be signed to protect against + /// tampering. + /// </remarks> + public IDictionary<string, string> GetUntrustedCallbackArguments() { + var args = new Dictionary<string, string>(); + + // Return all the return_to arguments, except for the OpenID-supporting ones. + // The only arguments that should be returned here are the ones that the host + // web site adds explicitly. + foreach (string key in this.response.GetReturnToParameterNames().Where(key => !OpenIdRelyingParty.IsOpenIdSupportingParameter(key))) { + args[key] = this.response.GetReturnToArgument(key); + } + + return args; + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension<T>"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetExtension<T>() where T : IOpenIdMessageExtension { + return this.response.SignedExtensions.OfType<T>().FirstOrDefault(); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetExtension(Type extensionType) { + return this.response.SignedExtensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault(); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response, without + /// requiring it to be signed by the Provider. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension<T>"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension { + return this.response.Extensions.OfType<T>().FirstOrDefault(); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) { + return this.response.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs new file mode 100644 index 0000000..1123912 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -0,0 +1,174 @@ +//----------------------------------------------------------------------- +// <copyright file="PositiveAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Wraps a positive assertion response in an <see cref="IAuthenticationResponse"/> instance + /// for public consumption by the host web site. + /// </summary> + [DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")] + internal class PositiveAuthenticationResponse : PositiveAnonymousResponse { + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAuthenticationResponse"/> class. + /// </summary> + /// <param name="response">The positive assertion response that was just received by the Relying Party.</param> + /// <param name="relyingParty">The relying party.</param> + internal PositiveAuthenticationResponse(PositiveAssertionResponse response, OpenIdRelyingParty relyingParty) + : base(response) { + Requires.NotNull(relyingParty, "relyingParty"); + + this.Endpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier( + this.Response.ClaimedIdentifier, + this.Response.GetReturnToArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName), + this.Response.LocalIdentifier, + new ProviderEndpointDescription(this.Response.ProviderEndpoint, this.Response.Version), + null, + null); + + this.VerifyDiscoveryMatchesAssertion(relyingParty); + + Logger.OpenId.InfoFormat("Received identity assertion for {0} via {1}.", this.Response.ClaimedIdentifier, this.Provider.Uri); + } + + #region IAuthenticationResponse Properties + + /// <summary> + /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. + /// May be null for some failed authentications (i.e. failed directed identity authentications). + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This is the secure identifier that should be used for database storage and lookup. + /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects + /// user identities against spoofing and other attacks. + /// </para> + /// <para> + /// For user-friendly identifiers to display, use the + /// <see cref="FriendlyIdentifierForDisplay"/> property. + /// </para> + /// </remarks> + public override Identifier ClaimedIdentifier { + get { return this.Endpoint.ClaimedIdentifier; } + } + + /// <summary> + /// Gets a user-friendly OpenID Identifier for display purposes ONLY. + /// </summary> + /// <remarks> + /// <para> + /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before + /// sending to a browser to secure against javascript injection attacks. + /// </para> + /// <para> + /// This property retains some aspects of the user-supplied identifier that get lost + /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied + /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD). + /// For display purposes, such as text on a web page that says "You're logged in as ...", + /// this property serves to provide the =Arnott string, or whatever else is the most friendly + /// string close to what the user originally typed in. + /// </para> + /// <para> + /// If the user-supplied identifier is a URI, this property will be the URI after all + /// redirects, and with the protocol and fragment trimmed off. + /// If the user-supplied identifier is an XRI, this property will be the original XRI. + /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com), + /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI. + /// </para> + /// <para> + /// It is <b>very</b> important that this property <i>never</i> be used for database storage + /// or lookup to avoid identity spoofing and other security risks. For database storage + /// and lookup please use the <see cref="ClaimedIdentifier"/> property. + /// </para> + /// </remarks> + public override string FriendlyIdentifierForDisplay { + get { return this.Endpoint.FriendlyIdentifierForDisplay; } + } + + /// <summary> + /// Gets the detailed success or failure status of the authentication attempt. + /// </summary> + public override AuthenticationStatus Status { + get { return AuthenticationStatus.Authenticated; } + } + + #endregion + + /// <summary> + /// Gets the OpenID service endpoint reconstructed from the assertion message. + /// </summary> + /// <remarks> + /// This information is straight from the Provider, and therefore must not + /// be trusted until verified as matching the discovery information for + /// the claimed identifier to avoid a Provider asserting an Identifier + /// for which it has no authority. + /// </remarks> + internal IdentifierDiscoveryResult Endpoint { get; private set; } + + /// <summary> + /// Gets the positive assertion response message. + /// </summary> + protected internal new PositiveAssertionResponse Response { + get { return (PositiveAssertionResponse)base.Response; } + } + + /// <summary> + /// Verifies that the positive assertion data matches the results of + /// discovery on the Claimed Identifier. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + /// <exception cref="ProtocolException"> + /// Thrown when the Provider is asserting that a user controls an Identifier + /// when discovery on that Identifier contradicts what the Provider says. + /// This would be an indication of either a misconfigured Provider or + /// an attempt by someone to spoof another user's identity with a rogue Provider. + /// </exception> + private void VerifyDiscoveryMatchesAssertion(OpenIdRelyingParty relyingParty) { + Logger.OpenId.Debug("Verifying assertion matches identifier discovery results..."); + + // Ensure that we abide by the RP's rules regarding RequireSsl for this discovery step. + Identifier claimedId = this.Response.ClaimedIdentifier; + if (relyingParty.SecuritySettings.RequireSsl) { + if (!claimedId.TryRequireSsl(out claimedId)) { + Logger.OpenId.ErrorFormat("This site is configured to accept only SSL-protected OpenIDs, but {0} was asserted and must be rejected.", this.Response.ClaimedIdentifier); + ErrorUtilities.ThrowProtocol(OpenIdStrings.RequireSslNotSatisfiedByAssertedClaimedId, this.Response.ClaimedIdentifier); + } + } + + // Check whether this particular identifier presents a problem with HTTP discovery + // due to limitations in the .NET Uri class. + UriIdentifier claimedIdUri = claimedId as UriIdentifier; + if (claimedIdUri != null && claimedIdUri.ProblematicNormalization) { + ErrorUtilities.VerifyProtocol(relyingParty.SecuritySettings.AllowApproximateIdentifierDiscovery, OpenIdStrings.ClaimedIdentifierDefiesDotNetNormalization); + Logger.OpenId.WarnFormat("Positive assertion for claimed identifier {0} cannot be precisely verified under partial trust hosting due to .NET limitation. An approximate verification will be attempted.", claimedId); + } + + // While it LOOKS like we're performing discovery over HTTP again + // Yadis.IdentifierDiscoveryCachePolicy is set to HttpRequestCacheLevel.CacheIfAvailable + // which means that the .NET runtime is caching our discoveries for us. This turns out + // to be very fast and keeps our code clean and easily verifiable as correct and secure. + // CAUTION: if this discovery is ever made to be skipped based on previous discovery + // data that was saved to the return_to URL, be careful to verify that that information + // is signed by the RP before it's considered reliable. In 1.x stateless mode, this RP + // doesn't (and can't) sign its own return_to URL, so its cached discovery information + // is merely a hint that must be verified by performing discovery again here. + var discoveryResults = relyingParty.Discover(claimedId); + ErrorUtilities.VerifyProtocol( + discoveryResults.Contains(this.Endpoint), + OpenIdStrings.IssuedAssertionFailsIdentifierDiscovery, + this.Endpoint, + discoveryResults.ToStringDeferred(true)); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs new file mode 100644 index 0000000..141b4f7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs @@ -0,0 +1,304 @@ +//----------------------------------------------------------------------- +// <copyright file="PositiveAuthenticationResponseSnapshot.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A serializable snapshot of a verified authentication message. + /// </summary> + [Serializable] + internal class PositiveAuthenticationResponseSnapshot : IAuthenticationResponse { + /// <summary> + /// The callback arguments that came with the authentication response. + /// </summary> + private IDictionary<string, string> callbackArguments; + + /// <summary> + /// The untrusted callback arguments that came with the authentication response. + /// </summary> + private IDictionary<string, string> untrustedCallbackArguments; + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAuthenticationResponseSnapshot"/> class. + /// </summary> + /// <param name="copyFrom">The authentication response to copy from.</param> + internal PositiveAuthenticationResponseSnapshot(IAuthenticationResponse copyFrom) { + Requires.NotNull(copyFrom, "copyFrom"); + + this.ClaimedIdentifier = copyFrom.ClaimedIdentifier; + this.FriendlyIdentifierForDisplay = copyFrom.FriendlyIdentifierForDisplay; + this.Status = copyFrom.Status; + this.Provider = copyFrom.Provider; + this.untrustedCallbackArguments = copyFrom.GetUntrustedCallbackArguments(); + + // Do this special check to avoid logging a warning for trying to clone a dictionary. + var anonResponse = copyFrom as PositiveAnonymousResponse; + if (anonResponse == null || anonResponse.TrustedCallbackArgumentsAvailable) { + this.callbackArguments = copyFrom.GetCallbackArguments(); + } else { + this.callbackArguments = EmptyDictionary<string, string>.Instance; + } + } + + #region IAuthenticationResponse Members + + /// <summary> + /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. + /// May be null for some failed authentications (i.e. failed directed identity authentications). + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This is the secure identifier that should be used for database storage and lookup. + /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects + /// user identities against spoofing and other attacks. + /// </para> + /// <para> + /// For user-friendly identifiers to display, use the + /// <see cref="FriendlyIdentifierForDisplay"/> property. + /// </para> + /// </remarks> + public Identifier ClaimedIdentifier { get; private set; } + + /// <summary> + /// Gets a user-friendly OpenID Identifier for display purposes ONLY. + /// </summary> + /// <value></value> + /// <remarks> + /// <para> + /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before + /// sending to a browser to secure against javascript injection attacks. + /// </para> + /// <para> + /// This property retains some aspects of the user-supplied identifier that get lost + /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied + /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD). + /// For display purposes, such as text on a web page that says "You're logged in as ...", + /// this property serves to provide the =Arnott string, or whatever else is the most friendly + /// string close to what the user originally typed in. + /// </para> + /// <para> + /// If the user-supplied identifier is a URI, this property will be the URI after all + /// redirects, and with the protocol and fragment trimmed off. + /// If the user-supplied identifier is an XRI, this property will be the original XRI. + /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com), + /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI. + /// </para> + /// <para> + /// It is <b>very</b> important that this property <i>never</i> be used for database storage + /// or lookup to avoid identity spoofing and other security risks. For database storage + /// and lookup please use the <see cref="ClaimedIdentifier"/> property. + /// </para> + /// </remarks> + public string FriendlyIdentifierForDisplay { get; private set; } + + /// <summary> + /// Gets the detailed success or failure status of the authentication attempt. + /// </summary> + /// <value></value> + public AuthenticationStatus Status { get; private set; } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + /// <value> + /// The Provider endpoint that issued the positive assertion; + /// or <c>null</c> if information about the Provider is unavailable. + /// </value> + public IProviderEndpoint Provider { get; private set; } + + /// <summary> + /// Gets the details regarding a failed authentication attempt, if available. + /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>. + /// </summary> + /// <value></value> + public Exception Exception { + get { throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); } + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension<T>"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetExtension<T>() where T : IOpenIdMessageExtension { + throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned only if the Provider signed them. + /// Relying parties that do not care if the values were modified in + /// transit should use the <see cref="GetUntrustedExtension"/> method + /// in order to allow the Provider to not sign the extension. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetExtension(Type extensionType) { + throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response, without + /// requiring it to be signed by the Provider. + /// </summary> + /// <typeparam name="T">The type of extension to look for in the response message.</typeparam> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension<T>"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension { + throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); + } + + /// <summary> + /// Tries to get an OpenID extension that may be present in the response. + /// </summary> + /// <param name="extensionType">Type of the extension to look for in the response.</param> + /// <returns> + /// The extension, if it is found. Null otherwise. + /// </returns> + /// <remarks> + /// <para>Extensions are returned whether they are signed or not. + /// Use the <see cref="GetExtension"/> method to retrieve + /// extension responses only if they are signed by the Provider to + /// protect against tampering. </para> + /// <para>Unsigned extensions are completely unreliable and should be + /// used only to prefill user forms since the user or any other third + /// party may have tampered with the data carried by the extension.</para> + /// <para>Signed extensions are only reliable if the relying party + /// trusts the OpenID Provider that signed them. Signing does not mean + /// the relying party can trust the values -- it only means that the values + /// have not been tampered with since the Provider sent the message.</para> + /// </remarks> + public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) { + throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// <para>This MAY return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public IDictionary<string, string> GetCallbackArguments() { + // Return a copy so that the caller cannot change the contents. + return new Dictionary<string, string>(this.callbackArguments); + } + + /// <summary> + /// Gets all the callback arguments that were previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part + /// of the return_to URL. + /// </summary> + /// <returns>A name-value dictionary. Never null.</returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public IDictionary<string, string> GetUntrustedCallbackArguments() { + // Return a copy so that the caller cannot change the contents. + return new Dictionary<string, string>(this.untrustedCallbackArguments); + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// <para>This may return any argument on the querystring that came with the authentication response, + /// which may include parameters not explicitly added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para> + /// <para>Note that these values are NOT protected against tampering in transit.</para> + /// </remarks> + public string GetCallbackArgument(string key) { + string value; + this.callbackArguments.TryGetValue(key, out value); + return value; + } + + /// <summary> + /// Gets a callback argument's value that was previously added using + /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>. + /// </summary> + /// <param name="key">The name of the parameter whose value is sought.</param> + /// <returns> + /// The value of the argument, or null if the named parameter could not be found. + /// </returns> + /// <remarks> + /// Callback parameters are only available even if the RP is in stateless mode, + /// or the callback parameters are otherwise unverifiable as untampered with. + /// Therefore, use this method only when the callback argument is not to be + /// used to make a security-sensitive decision. + /// </remarks> + public string GetUntrustedCallbackArgument(string key) { + string value; + this.untrustedCallbackArguments.TryGetValue(key, out value); + return value; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs new file mode 100644 index 0000000..678f69a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------- +// <copyright file="SimpleXrdsProviderEndpoint.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.ObjectModel; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A very simple IXrdsProviderEndpoint implementation for verifying that all positive + /// assertions (particularly unsolicited ones) are received from OP endpoints that + /// are deemed permissible by the host RP. + /// </summary> + internal class SimpleXrdsProviderEndpoint : IProviderEndpoint { + /// <summary> + /// Initializes a new instance of the <see cref="SimpleXrdsProviderEndpoint"/> class. + /// </summary> + /// <param name="positiveAssertion">The positive assertion.</param> + internal SimpleXrdsProviderEndpoint(PositiveAssertionResponse positiveAssertion) { + this.Uri = positiveAssertion.ProviderEndpoint; + this.Version = positiveAssertion.Version; + } + + #region IProviderEndpoint Members + + /// <summary> + /// Gets the detected version of OpenID implemented by the Provider. + /// </summary> + public Version Version { get; private set; } + + /// <summary> + /// Gets the URL that the OpenID Provider receives authentication requests at. + /// </summary> + public Uri Uri { get; private set; } + + /// <summary> + /// Checks whether the OpenId Identifier claims support for a given extension. + /// </summary> + /// <typeparam name="T">The extension whose support is being queried.</typeparam> + /// <returns> + /// True if support for the extension is advertised. False otherwise. + /// </returns> + /// <remarks> + /// Note that a true or false return value is no guarantee of a Provider's + /// support for or lack of support for an extension. The return value is + /// determined by how the authenticating user filled out his/her XRDS document only. + /// The only way to be sure of support for a given extension is to include + /// the extension in the request and see if a response comes back for that extension. + /// </remarks> + bool IProviderEndpoint.IsExtensionSupported<T>() { + throw new NotImplementedException(); + } + + /// <summary> + /// Checks whether the OpenId Identifier claims support for a given extension. + /// </summary> + /// <param name="extensionType">The extension whose support is being queried.</param> + /// <returns> + /// True if support for the extension is advertised. False otherwise. + /// </returns> + /// <remarks> + /// Note that a true or false return value is no guarantee of a Provider's + /// support for or lack of support for an extension. The return value is + /// determined by how the authenticating user filled out his/her XRDS document only. + /// The only way to be sure of support for a given extension is to include + /// the extension in the request and see if a response comes back for that extension. + /// </remarks> + bool IProviderEndpoint.IsExtensionSupported(Type extensionType) { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs new file mode 100644 index 0000000..a14b55d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardRelyingPartyApplicationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// An in-memory store for Relying Parties, suitable for single server, single process + /// ASP.NET web sites. + /// </summary> + public class StandardRelyingPartyApplicationStore : IOpenIdApplicationStore { + /// <summary> + /// The nonce store to use. + /// </summary> + private readonly INonceStore nonceStore; + + /// <summary> + /// The association store to use. + /// </summary> + private readonly ICryptoKeyStore keyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="StandardRelyingPartyApplicationStore"/> class. + /// </summary> + public StandardRelyingPartyApplicationStore() { + this.nonceStore = new NonceMemoryStore(OpenIdElement.Configuration.MaxAuthenticationTime); + this.keyStore = new MemoryCryptoKeyStore(); + } + + #region ICryptoKeyStore Members + + /// <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.keyStore.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.keyStore.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.keyStore.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.keyStore.RemoveKey(bucket, handle); + } + + #endregion + + #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 + } +} diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs new file mode 100644 index 0000000..ad1a11a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// <copyright file="WellKnownProviders.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System.Diagnostics.CodeAnalysis; + + /// <summary> + /// Common OpenID Provider Identifiers. + /// </summary> + public sealed class WellKnownProviders { + /// <summary> + /// The Yahoo OP Identifier. + /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")] + public static readonly Identifier Yahoo = "https://me.yahoo.com/"; + + /// <summary> + /// The Google OP Identifier. + /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")] + public static readonly Identifier Google = "https://www.google.com/accounts/o8/id"; + + /// <summary> + /// The MyOpenID OP Identifier. + /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")] + public static readonly Identifier MyOpenId = "https://www.myopenid.com/"; + + /// <summary> + /// The Verisign OP Identifier. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Verisign", Justification = "The spelling is correct.")] + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")] + public static readonly Identifier Verisign = "https://pip.verisignlabs.com/"; + + /// <summary> + /// The MyVidoop OP Identifier. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Vidoop", Justification = "The spelling is correct.")] + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")] + public static readonly Identifier MyVidoop = "https://myvidoop.com/"; + + /// <summary> + /// Prevents a default instance of the <see cref="WellKnownProviders"/> class from being created. + /// </summary> + private WellKnownProviders() { + } + } +} |