//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.Provider { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.ChannelElements; using DotNetOpenAuth.OpenId.Messages; using Validation; using RP = DotNetOpenAuth.OpenId.RelyingParty; /// /// Offers services for a web page that is acting as an OpenID identity server. /// [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")] public sealed class OpenIdProvider : IDisposable, IOpenIdHost { /// /// The name of the key to use in the HttpApplication cache to store the /// instance of to use. /// private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.Provider.OpenIdProvider.ApplicationStore"; /// /// Backing store for the property. /// private readonly ObservableCollection behaviors = new ObservableCollection(); /// /// The discovery service used to perform discovery on identifiers being sent in /// unsolicited positive assertions. /// private readonly IdentifierDiscoveryServices discoveryServices; /// /// Backing field for the property. /// private ProviderSecuritySettings securitySettings; /// /// Initializes a new instance of the class. /// public OpenIdProvider() : this(OpenIdElement.Configuration.Provider.ApplicationStore.CreateInstance(GetHttpApplicationStore(), null)) { } /// /// Initializes a new instance of the class. /// /// The application store to use. Cannot be null. /// The host factories. public OpenIdProvider(ICryptoKeyAndNonceStore applicationStore, IHostFactories hostFactories = null) : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore, hostFactories) { Requires.NotNull(applicationStore, "applicationStore"); } /// /// Initializes a new instance of the class. /// /// The nonce store to use. Cannot be null. /// The crypto key store. Cannot be null. /// The host factories. private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore, IHostFactories hostFactories) { Requires.NotNull(nonceStore, "nonceStore"); Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); this.SecuritySettings = OpenIdElement.Configuration.Provider.SecuritySettings.CreateSecuritySettings(); this.behaviors.CollectionChanged += this.OnBehaviorsChanged; foreach (var behavior in OpenIdElement.Configuration.Provider.Behaviors.CreateInstances(false, null)) { this.behaviors.Add(behavior); } this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings); this.Channel = new OpenIdProviderChannel(this.AssociationStore, nonceStore, this.SecuritySettings, hostFactories); this.CryptoKeyStore = cryptoKeyStore; this.discoveryServices = new IdentifierDiscoveryServices(this); Reporting.RecordFeatureAndDependencyUse(this, nonceStore); } /// /// Gets the channel to use for sending/receiving messages. /// public Channel Channel { get; internal set; } /// /// Gets the security settings used by this Provider. /// public ProviderSecuritySettings SecuritySettings { get { Assumes.True(this.securitySettings != null); return this.securitySettings; } internal set { Requires.NotNull(value, "value"); this.securitySettings = value; } } /// /// Gets the security settings. /// SecuritySettings IOpenIdHost.SecuritySettings { get { return this.SecuritySettings; } } /// /// Gets the extension factories. /// public IList ExtensionFactories { get { return this.Channel.GetExtensionFactories(); } } /// /// Gets or sets the mechanism a host site can use to receive /// notifications of errors when communicating with remote parties. /// public IErrorReporting ErrorReporting { get; set; } /// /// Gets a list of custom behaviors to apply to OpenID actions. /// /// /// Adding behaviors can impact the security settings of the /// in ways that subsequently removing the behaviors will not reverse. /// public ICollection Behaviors { get { return this.behaviors; } } /// /// Gets the crypto key store. /// public ICryptoKeyStore CryptoKeyStore { get; private set; } /// /// Gets the factory for various dependencies. /// IHostFactories IOpenIdHost.HostFactories { get { return this.Channel.HostFactories; } } /// /// Gets the association store. /// internal IProviderAssociationStore AssociationStore { get; private set; } /// /// Gets the channel. /// internal OpenIdChannel OpenIdChannel { get { return (OpenIdChannel)this.Channel; } } /// /// Gets the list of services that can perform discovery on identifiers given. /// internal IList DiscoveryServices { get { return this.discoveryServices.DiscoveryServices; } } /// /// Gets the standard state storage mechanism that uses ASP.NET's /// HttpApplication state dictionary to store associations and nonces. /// /// The context. /// The application store. public static ICryptoKeyAndNonceStore GetHttpApplicationStore(HttpContextBase context = null) { if (context == null) { ErrorUtilities.VerifyOperation(HttpContext.Current != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(ICryptoKeyAndNonceStore).Name); context = new HttpContextWrapper(HttpContext.Current); } var store = (ICryptoKeyAndNonceStore)context.Application[ApplicationStoreKey]; if (store == null) { context.Application.Lock(); try { if ((store = (ICryptoKeyAndNonceStore)context.Application[ApplicationStoreKey]) == null) { context.Application[ApplicationStoreKey] = store = new MemoryCryptoKeyAndNonceStore(); } } finally { context.Application.UnLock(); } } return store; } /// /// Gets the incoming OpenID request if there is one, or null if none was detected. /// /// The request. /// The cancellation token. /// /// The request that the hosting Provider should possibly process and then transmit the response for. /// /// Thrown if HttpContext.Current == null. /// Thrown if the incoming message is recognized but deviates from the protocol specification irrecoverably. /// /// Requests may be infrastructural to OpenID and allow auto-responses, or they may /// be authentication requests where the Provider site has to make decisions based /// on its own user database and policies. /// Requires an HttpContext.Current context. /// public Task GetRequestAsync(HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { request = request ?? this.Channel.GetRequestFromContext(); return this.GetRequestAsync(request.AsHttpRequestMessage(), cancellationToken); } /// /// Gets the incoming OpenID request if there is one, or null if none was detected. /// /// The incoming HTTP request to extract the message from. /// The cancellation token. /// /// The request that the hosting Provider should process and then transmit the response for. /// Null if no valid OpenID request was detected in the given HTTP request. /// /// Thrown if the incoming message is recognized /// but deviates from the protocol specification irrecoverably. /// /// Requests may be infrastructural to OpenID and allow auto-responses, or they may /// be authentication requests where the Provider site has to make decisions based /// on its own user database and policies. /// public async Task GetRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(request, "request"); IDirectedProtocolMessage incomingMessage = null; try { incomingMessage = await this.Channel.ReadFromRequestAsync(request, cancellationToken); if (incomingMessage == null) { // If the incoming request does not resemble an OpenID message at all, // it's probably a user who just navigated to this URL, and we should // just return null so the host can display a message to the user. if (request.Method == HttpMethod.Get && !request.RequestUri.QueryStringContainPrefixedParameters(Protocol.Default.openid.Prefix)) { return null; } ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); } IRequest result = null; var checkIdMessage = incomingMessage as CheckIdRequest; if (checkIdMessage != null) { result = new AuthenticationRequest(this, checkIdMessage); } if (result == null) { var extensionOnlyRequest = incomingMessage as SignedResponseRequest; if (extensionOnlyRequest != null) { result = new AnonymousRequest(this, extensionOnlyRequest); } } if (result == null) { var checkAuthMessage = incomingMessage as CheckAuthenticationRequest; if (checkAuthMessage != null) { result = new AutoResponsiveRequest(incomingMessage, new CheckAuthenticationResponseProvider(checkAuthMessage, this), this.SecuritySettings); } } if (result == null) { var associateMessage = incomingMessage as IAssociateRequestProvider; if (associateMessage != null) { result = new AutoResponsiveRequest(incomingMessage, AssociateRequestProviderTools.CreateResponse(associateMessage, this.AssociationStore, this.SecuritySettings), this.SecuritySettings); } } if (result != null) { foreach (var behavior in this.Behaviors) { if (await behavior.OnIncomingRequestAsync(result, cancellationToken)) { // This behavior matched this request. break; } } return result; } throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); } catch (ProtocolException ex) { IRequest errorResponse = this.GetErrorResponse(ex, request, incomingMessage); if (errorResponse == null) { throw; } return errorResponse; } } /// /// Gets the response to a received request. /// /// The request. /// The cancellation token. /// /// The response that should be sent to the client. /// /// Thrown if is false. [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] public async Task PrepareResponseAsync(IRequest request, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(request, "request"); Requires.That(request.IsResponseReady, "request", OpenIdStrings.ResponseNotReady); await this.ApplyBehaviorsToResponseAsync(request, cancellationToken); Request requestInternal = (Request)request; var response = await requestInternal.GetResponseAsync(cancellationToken); return await this.Channel.PrepareResponseAsync(response, cancellationToken); } /// /// Prepares an identity assertion on behalf of one of this Provider's /// members in order to redirect the user agent to a relying party /// web site and log him/her in immediately in one uninterrupted step. /// /// The absolute URL on the Provider site that receives OpenID messages. /// The URL of the Relying Party web site. /// This will typically be the home page, but may be a longer URL if /// that Relying Party considers the scope of its realm to be more specific. /// The URL provided here must allow discovery of the Relying Party's /// XRDS document that advertises its OpenID RP endpoint. /// The Identifier you are asserting your member controls. /// The Identifier you know your user by internally. This will typically /// be the same as . /// The cancellation token. /// The extensions. /// /// A object describing the HTTP response to send /// the user agent to allow the redirect with assertion to happen. /// public async Task PrepareUnsolicitedAssertionAsync(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, CancellationToken cancellationToken = default(CancellationToken), params IExtensionMessage[] extensions) { Requires.NotNull(providerEndpoint, "providerEndpoint"); Requires.That(providerEndpoint.IsAbsoluteUri, "providerEndpoint", OpenIdStrings.AbsoluteUriRequired); Requires.NotNull(relyingPartyRealm, "relyingPartyRealm"); Requires.NotNull(claimedIdentifier, "claimedIdentifier"); Requires.NotNull(localIdentifier, "localIdentifier"); RequiresEx.ValidState(this.Channel.HostFactories != null); // Although the RP should do their due diligence to make sure that this OP // is authorized to send an assertion for the given claimed identifier, // do due diligence by performing our own discovery on the claimed identifier // and make sure that it is tied to this OP and OP local identifier. if (this.SecuritySettings.UnsolicitedAssertionVerification != ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.NeverVerify) { var serviceEndpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null); var discoveredEndpoints = await this.discoveryServices.DiscoverAsync(claimedIdentifier, cancellationToken); if (!discoveredEndpoints.Contains(serviceEndpoint)) { Logger.OpenId.WarnFormat( "Failed to send unsolicited assertion for {0} because its discovered services did not include this endpoint: {1}{2}{1}Discovered endpoints: {1}{3}", claimedIdentifier, Environment.NewLine, serviceEndpoint, discoveredEndpoints.ToStringDeferred(true)); // Only FAIL if the setting is set for it. if (this.securitySettings.UnsolicitedAssertionVerification == ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess) { ErrorUtilities.ThrowProtocol(OpenIdStrings.UnsolicitedAssertionForUnrelatedClaimedIdentifier, claimedIdentifier); } } } Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier); RelyingPartyEndpointDescription returnToEndpoint = null; var returnToEndpoints = await relyingPartyRealm.DiscoverReturnToEndpointsAsync(this.Channel.HostFactories, true, cancellationToken); if (returnToEndpoints != null) { returnToEndpoint = returnToEndpoints.FirstOrDefault(); } ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm); var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) { ProviderEndpoint = providerEndpoint, ClaimedIdentifier = claimedIdentifier, LocalIdentifier = localIdentifier, }; if (extensions != null) { foreach (IExtensionMessage extension in extensions) { positiveAssertion.Extensions.Add(extension); } } Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); return await this.Channel.PrepareResponseAsync(positiveAssertion, cancellationToken); } #region IDisposable Members /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. private void Dispose(bool disposing) { if (disposing) { // Tear off the instance member as a local variable for thread safety. IDisposable channel = this.Channel as IDisposable; if (channel != null) { channel.Dispose(); } } } #endregion /// /// Applies all behaviors to the response message. /// /// The request. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// private async Task ApplyBehaviorsToResponseAsync(IRequest request, CancellationToken cancellationToken) { var authRequest = request as IAuthenticationRequest; if (authRequest != null) { foreach (var behavior in this.Behaviors) { if (await behavior.OnOutgoingResponseAsync(authRequest, cancellationToken)) { // This behavior matched this request. break; } } } } /// /// Prepares the return value for the GetRequest method in the event of an exception. /// /// The exception that forms the basis of the error response. Must not be null. /// The incoming HTTP request. Must not be null. /// The incoming message. May be null in the case that it was malformed. /// /// Either the to return to the host site or null to indicate no response could be reasonably created and that the caller should rethrow the exception. /// private IRequest GetErrorResponse(ProtocolException ex, HttpRequestMessage request, IDirectedProtocolMessage incomingMessage) { Requires.NotNull(ex, "ex"); Requires.NotNull(request, "request"); Logger.OpenId.ErrorException("An exception was generated while processing an incoming OpenID request.", ex); IErrorMessage errorMessage; // We must create the appropriate error message type (direct vs. indirect) // based on what we see in the request. string returnTo = HttpUtility.ParseQueryString(request.RequestUri.Query)[Protocol.Default.openid.return_to]; if (returnTo != null) { // An indirect request message from the RP // We need to return an indirect response error message so the RP can consume it. // Consistent with OpenID 2.0 section 5.2.3. var indirectRequest = incomingMessage as SignedResponseRequest; if (indirectRequest != null) { errorMessage = new IndirectErrorResponse(indirectRequest); } else { errorMessage = new IndirectErrorResponse(Protocol.Default.Version, new Uri(returnTo)); } } else if (request.Method == HttpMethod.Post) { // A direct request message from the RP // We need to return a direct response error message so the RP can consume it. // Consistent with OpenID 2.0 section 5.1.2.2. errorMessage = new DirectErrorResponse(Protocol.Default.Version, incomingMessage); } else { // This may be an indirect request from an RP that was so badly // formed that we cannot even return an error to the RP. // The best we can do is display an error to the user. // Returning null cues the caller to "throw;" return null; } errorMessage.ErrorMessage = ex.ToStringDescriptive(); // Allow host to log this error and issue a ticket #. // We tear off the field to a local var for thread safety. IErrorReporting hostErrorHandler = this.ErrorReporting; if (hostErrorHandler != null) { errorMessage.Contact = hostErrorHandler.Contact; errorMessage.Reference = hostErrorHandler.LogError(ex); } if (incomingMessage != null) { return new AutoResponsiveRequest(incomingMessage, errorMessage, this.SecuritySettings); } else { return new AutoResponsiveRequest(errorMessage, this.SecuritySettings); } } /// /// Called by derived classes when behaviors are added or removed. /// /// The collection being modified. /// The instance containing the event data. private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) { foreach (IProviderBehavior profile in e.NewItems) { profile.ApplySecuritySettings(this.SecuritySettings); Reporting.RecordFeatureUse(profile); } } /// /// Provides a single OP association store instance that can handle switching between /// association handle encoding modes. /// private class SwitchingAssociationStore : IProviderAssociationStore { /// /// The security settings of the Provider. /// private readonly ProviderSecuritySettings securitySettings; /// /// The association store that records association secrets in the association handles themselves. /// private IProviderAssociationStore associationHandleEncoder; /// /// The association store that records association secrets in a secret store. /// private IProviderAssociationStore associationSecretStorage; /// /// Initializes a new instance of the class. /// /// The crypto key store. /// The security settings. internal SwitchingAssociationStore(ICryptoKeyStore cryptoKeyStore, ProviderSecuritySettings securitySettings) { Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); Requires.NotNull(securitySettings, "securitySettings"); this.securitySettings = securitySettings; this.associationHandleEncoder = new ProviderAssociationHandleEncoder(cryptoKeyStore); this.associationSecretStorage = new ProviderAssociationKeyStorage(cryptoKeyStore); } /// /// Gets the association store that applies given the Provider's current security settings. /// internal IProviderAssociationStore AssociationStore { get { return this.securitySettings.EncodeAssociationSecretsInHandles ? this.associationHandleEncoder : this.associationSecretStorage; } } /// /// Stores an association and returns a handle for it. /// /// The association secret. /// The UTC time that the association should expire. /// A value indicating whether this is a private association. /// /// The association handle that represents this association. /// public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { return this.AssociationStore.Serialize(secret, expiresUtc, privateAssociation); } /// /// Retrieves an association given an association handle. /// /// The OpenID message that referenced this association handle. /// A value indicating whether a private association is expected. /// The association handle. /// /// An association instance, or null if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). /// /// Thrown if the association is not of the expected type. public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) { return this.AssociationStore.Deserialize(containingMessage, isPrivateAssociation, handle); } } } }