//----------------------------------------------------------------------- // // 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.Diagnostics.Contracts; using System.Linq; using System.Threading; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.ChannelElements; using DotNetOpenAuth.OpenId.Messages; using RP = DotNetOpenAuth.OpenId.RelyingParty; /// /// Offers services for a web page that is acting as an OpenID identity server. /// [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")] [ContractVerification(true)] 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(HttpApplicationStore)) { Contract.Ensures(this.SecuritySettings != null); Contract.Ensures(this.Channel != null); } /// /// Initializes a new instance of the class. /// /// The application store to use. Cannot be null. public OpenIdProvider(IOpenIdApplicationStore applicationStore) : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore) { Requires.NotNull(applicationStore, "applicationStore"); Contract.Ensures(this.SecuritySettings != null); Contract.Ensures(this.Channel != null); } /// /// Initializes a new instance of the class. /// /// The nonce store to use. Cannot be null. /// The crypto key store. Cannot be null. private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) { Requires.NotNull(nonceStore, "nonceStore"); Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); Contract.Ensures(this.SecuritySettings != null); Contract.Ensures(this.Channel != null); this.SecuritySettings = OpenIdElement.Configuration.Provider.SecuritySettings.CreateSecuritySettings(); this.behaviors.CollectionChanged += this.OnBehaviorsChanged; foreach (var behavior in OpenIdElement.Configuration.Provider.Behaviors.CreateInstances(false)) { this.behaviors.Add(behavior); } this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings); this.Channel = new OpenIdProviderChannel(this.AssociationStore, nonceStore, this.SecuritySettings); this.CryptoKeyStore = cryptoKeyStore; this.discoveryServices = new IdentifierDiscoveryServices(this); Reporting.RecordFeatureAndDependencyUse(this, nonceStore); } /// /// Gets the standard state storage mechanism that uses ASP.NET's /// HttpApplication state dictionary to store associations and nonces. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public static IOpenIdApplicationStore HttpApplicationStore { get { Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); Contract.Ensures(Contract.Result() != null); HttpContext context = HttpContext.Current; var store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey]; if (store == null) { context.Application.Lock(); try { if ((store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey]) == null) { context.Application[ApplicationStoreKey] = store = new StandardProviderApplicationStore(); } } finally { context.Application.UnLock(); } } return store; } } /// /// 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 { Contract.Ensures(Contract.Result() != null); Contract.Assume(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 web request handler to use for discovery and the part of /// authentication where direct messages are sent to an untrusted remote party. /// IDirectWebRequestHandler IOpenIdHost.WebRequestHandler { get { return this.Channel.WebRequestHandler; } } /// /// 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 web request handler to use for discovery and the part of /// authentication where direct messages are sent to an untrusted remote party. /// internal IDirectWebRequestHandler WebRequestHandler { get { return this.Channel.WebRequestHandler; } } /// /// Gets the incoming OpenID request if there is one, or null if none was detected. /// /// The request that the hosting Provider should possibly process and then transmit the response for. /// /// 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. /// /// Thrown if HttpContext.Current == null. /// Thrown if the incoming message is recognized but deviates from the protocol specification irrecoverably. public IRequest GetRequest() { return this.GetRequest(this.Channel.GetRequestFromContext()); } /// /// 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 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. /// /// /// 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. /// /// Thrown if the incoming message is recognized /// but deviates from the protocol specification irrecoverably. public IRequest GetRequest(HttpRequestBase httpRequestInfo) { Requires.NotNull(httpRequestInfo, "httpRequestInfo"); IDirectedProtocolMessage incomingMessage = null; try { incomingMessage = this.Channel.ReadFromRequest(httpRequestInfo); if (incomingMessage == null) { // If the incoming request does not resemble an OpenID message at all, // it's probably a user who just navigated to this URL, and we should // just return null so the host can display a message to the user. if (httpRequestInfo.HttpMethod == "GET" && !httpRequestInfo.GetPublicFacingUrl().QueryStringContainPrefixedParameters(Protocol.Default.openid.Prefix)) { return null; } ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); } IRequest result = null; var checkIdMessage = incomingMessage as CheckIdRequest; if (checkIdMessage != null) { result = new AuthenticationRequest(this, checkIdMessage); } if (result == null) { var extensionOnlyRequest = incomingMessage as SignedResponseRequest; if (extensionOnlyRequest != null) { result = new AnonymousRequest(this, extensionOnlyRequest); } } if (result == null) { var checkAuthMessage = incomingMessage as CheckAuthenticationRequest; if (checkAuthMessage != null) { result = new AutoResponsiveRequest(incomingMessage, new CheckAuthenticationResponseProvider(checkAuthMessage, this), this.SecuritySettings); } } if (result == null) { var associateMessage = incomingMessage as IAssociateRequestProvider; if (associateMessage != null) { result = new AutoResponsiveRequest(incomingMessage, AssociateRequestProviderTools.CreateResponse(associateMessage, this.AssociationStore, this.SecuritySettings), this.SecuritySettings); } } if (result != null) { foreach (var behavior in this.Behaviors) { if (behavior.OnIncomingRequest(result)) { // This behavior matched this request. break; } } return result; } throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); } catch (ProtocolException ex) { IRequest errorResponse = this.GetErrorResponse(ex, httpRequestInfo, incomingMessage); if (errorResponse == null) { throw; } return errorResponse; } } /// /// Sends the response to a received request. /// /// The incoming OpenID request whose response is to be sent. /// Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response. /// /// Requires an HttpContext.Current context. If one is not available, the caller should use /// instead and manually send the /// to the client. /// /// Thrown if is false. [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] [EditorBrowsable(EditorBrowsableState.Never)] public void SendResponse(IRequest request) { Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); Requires.NotNull(request, "request"); Requires.True(request.IsResponseReady, "request"); this.ApplyBehaviorsToResponse(request); Request requestInternal = (Request)request; this.Channel.Send(requestInternal.Response); } /// /// Sends the response to a received request. /// /// The incoming OpenID request whose response is to be sent. /// /// Requires an HttpContext.Current context. If one is not available, the caller should use /// instead and manually send the /// to the client. /// /// Thrown if is false. [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] public void Respond(IRequest request) { Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); Requires.NotNull(request, "request"); Requires.True(request.IsResponseReady, "request"); this.ApplyBehaviorsToResponse(request); Request requestInternal = (Request)request; this.Channel.Respond(requestInternal.Response); } /// /// Gets the response to a received request. /// /// The request. /// 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 OutgoingWebResponse PrepareResponse(IRequest request) { Requires.NotNull(request, "request"); Requires.True(request.IsResponseReady, "request"); this.ApplyBehaviorsToResponse(request); Request requestInternal = (Request)request; return this.Channel.PrepareResponse(requestInternal.Response); } /// /// Sends an identity assertion on behalf of one of this Provider's /// members in order to redirect the user agent to a relying party /// web site and log him/her in immediately in one uninterrupted step. /// /// 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 extensions. public void SendUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { Requires.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired); Requires.NotNull(providerEndpoint, "providerEndpoint"); Requires.True(providerEndpoint.IsAbsoluteUri, "providerEndpoint"); Requires.NotNull(relyingPartyRealm, "relyingPartyRealm"); Requires.NotNull(claimedIdentifier, "claimedIdentifier"); Requires.NotNull(localIdentifier, "localIdentifier"); this.PrepareUnsolicitedAssertion(providerEndpoint, relyingPartyRealm, claimedIdentifier, localIdentifier, extensions).Send(); } /// /// 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 extensions. /// /// A object describing the HTTP response to send /// the user agent to allow the redirect with assertion to happen. /// public OutgoingWebResponse PrepareUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { Requires.NotNull(providerEndpoint, "providerEndpoint"); Requires.True(providerEndpoint.IsAbsoluteUri, "providerEndpoint"); Requires.NotNull(relyingPartyRealm, "relyingPartyRealm"); Requires.NotNull(claimedIdentifier, "claimedIdentifier"); Requires.NotNull(localIdentifier, "localIdentifier"); Requires.ValidState(this.Channel.WebRequestHandler != null); // Although the RP should do their due diligence to make sure that this OP // is authorized to send an assertion for the given claimed identifier, // do due diligence by performing our own discovery on the claimed identifier // and make sure that it is tied to this OP and OP local identifier. if (this.SecuritySettings.UnsolicitedAssertionVerification != ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.NeverVerify) { var serviceEndpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null); var discoveredEndpoints = this.discoveryServices.Discover(claimedIdentifier); if (!discoveredEndpoints.Contains(serviceEndpoint)) { Logger.OpenId.WarnFormat( "Failed to send unsolicited assertion for {0} because its discovered services did not include this endpoint: {1}{2}{1}Discovered endpoints: {1}{3}", claimedIdentifier, Environment.NewLine, serviceEndpoint, discoveredEndpoints.ToStringDeferred(true)); // Only FAIL if the setting is set for it. if (this.securitySettings.UnsolicitedAssertionVerification == ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess) { ErrorUtilities.ThrowProtocol(OpenIdStrings.UnsolicitedAssertionForUnrelatedClaimedIdentifier, claimedIdentifier); } } } Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier); RelyingPartyEndpointDescription returnToEndpoint = null; var returnToEndpoints = relyingPartyRealm.DiscoverReturnToEndpoints(this.WebRequestHandler, true); if (returnToEndpoints != null) { returnToEndpoint = returnToEndpoints.FirstOrDefault(); } ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm); var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) { ProviderEndpoint = providerEndpoint, ClaimedIdentifier = claimedIdentifier, LocalIdentifier = localIdentifier, }; if (extensions != null) { foreach (IExtensionMessage extension in extensions) { positiveAssertion.Extensions.Add(extension); } } Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); return this.Channel.PrepareResponse(positiveAssertion); } #region IDisposable Members /// /// 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. private void ApplyBehaviorsToResponse(IRequest request) { var authRequest = request as IAuthenticationRequest; if (authRequest != null) { foreach (var behavior in this.Behaviors) { if (behavior.OnOutgoingResponse(authRequest)) { // This behavior matched this request. break; } } } } /// /// 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, HttpRequestBase httpRequestInfo, IDirectedProtocolMessage incomingMessage) { Requires.NotNull(ex, "ex"); Requires.NotNull(httpRequestInfo, "httpRequestInfo"); Logger.OpenId.Error("An exception was generated while processing an incoming OpenID request.", ex); IErrorMessage errorMessage; // We must create the appropriate error message type (direct vs. indirect) // based on what we see in the request. string returnTo = httpRequestInfo.QueryString[Protocol.Default.openid.return_to]; if (returnTo != null) { // An indirect request message from the RP // We need to return an indirect response error message so the RP can consume it. // Consistent with OpenID 2.0 section 5.2.3. var indirectRequest = incomingMessage as SignedResponseRequest; if (indirectRequest != null) { errorMessage = new IndirectErrorResponse(indirectRequest); } else { errorMessage = new IndirectErrorResponse(Protocol.Default.Version, new Uri(returnTo)); } } else if (httpRequestInfo.HttpMethod == "POST") { // A direct request message from the RP // We need to return a direct response error message so the RP can consume it. // Consistent with OpenID 2.0 section 5.1.2.2. errorMessage = new DirectErrorResponse(Protocol.Default.Version, incomingMessage); } else { // This may be an indirect request from an RP that was so badly // formed that we cannot even return an error to the RP. // The best we can do is display an error to the user. // Returning null cues the caller to "throw;" return null; } errorMessage.ErrorMessage = ex.ToStringDescriptive(); // Allow host to log this error and issue a ticket #. // We tear off the field to a local var for thread safety. IErrorReporting hostErrorHandler = this.ErrorReporting; if (hostErrorHandler != null) { errorMessage.Contact = hostErrorHandler.Contact; errorMessage.Reference = hostErrorHandler.LogError(ex); } if (incomingMessage != null) { return new AutoResponsiveRequest(incomingMessage, errorMessage, this.SecuritySettings); } else { return new AutoResponsiveRequest(errorMessage, this.SecuritySettings); } } /// /// 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); } } } }