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