//-----------------------------------------------------------------------
//
// 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);
}
}
}
}