summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId.Provider.UI/OpenId/Provider/ProviderEndpoint.cs
blob: bdee0d13f2c920c4328bb37475113cdc0fc1a2ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//-----------------------------------------------------------------------
// <copyright file="ProviderEndpoint.cs" company="Outercurve Foundation">
//     Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DotNetOpenAuth.OpenId.Provider {
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Net.Http;
	using System.Text;
	using System.Threading;
	using System.Threading.Tasks;
	using System.Web;
	using System.Web.UI;
	using System.Web.UI.WebControls;
	using DotNetOpenAuth.Configuration;
	using DotNetOpenAuth.Logging;
	using DotNetOpenAuth.Messaging;
	using DotNetOpenAuth.OpenId.Messages;
	using Validation;

	/// <summary>
	/// An OpenID Provider control that automatically responds to certain
	/// automated OpenID messages, and routes authentication requests to
	/// custom code via an event handler.
	/// </summary>
	[DefaultEvent("AuthenticationChallenge")]
	[ToolboxData("<{0}:ProviderEndpoint runat='server' />")]
	public class ProviderEndpoint : Control {
		/// <summary>
		/// The key used to store the pending authentication request in the ASP.NET session.
		/// </summary>
		private const string PendingRequestKey = "pendingRequest";

		/// <summary>
		/// The default value for the <see cref="Enabled"/> property.
		/// </summary>
		private const bool EnabledDefault = true;

		/// <summary>
		/// The view state key in which to store the value of the <see cref="Enabled"/> property.
		/// </summary>
		private const string EnabledViewStateKey = "Enabled";

		/// <summary>
		/// Backing field for the <see cref="Provider"/> property.
		/// </summary>
		private Lazy<OpenIdProvider> provider;

		/// <summary>
		/// Initializes a new instance of the <see cref="ProviderEndpoint"/> class.
		/// </summary>
		public ProviderEndpoint() {
			this.provider = new Lazy<OpenIdProvider>(this.CreateProvider);
		}

		/// <summary>
		/// Fired when an incoming OpenID request is an authentication challenge
		/// that must be responded to by the Provider web site according to its
		/// own user database and policies.
		/// </summary>
		public event EventHandler<AuthenticationChallengeEventArgs> AuthenticationChallenge;

		/// <summary>
		/// Fired when an incoming OpenID message carries extension requests
		/// but is not regarding any OpenID identifier.
		/// </summary>
		public event EventHandler<AnonymousRequestEventArgs> AnonymousRequest;

		/// <summary>
		/// Gets or sets an incoming OpenID authentication request that has not yet been responded to.
		/// </summary>
		/// <remarks>
		/// This request is stored in the ASP.NET Session state, so it will survive across
		/// redirects, postbacks, and transfers.  This allows you to authenticate the user
		/// yourself, and confirm his/her desire to authenticate to the relying party site
		/// before responding to the relying party's authentication request.
		/// </remarks>
		public static IAuthenticationRequest PendingAuthenticationRequest {
			get {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				return HttpContext.Current.Session[PendingRequestKey] as IAuthenticationRequest;
			}

			set {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				HttpContext.Current.Session[PendingRequestKey] = value;
			}
		}

		/// <summary>
		/// Gets or sets an incoming OpenID anonymous request that has not yet been responded to.
		/// </summary>
		/// <remarks>
		/// This request is stored in the ASP.NET Session state, so it will survive across
		/// redirects, postbacks, and transfers.  This allows you to authenticate the user
		/// yourself, and confirm his/her desire to provide data to the relying party site
		/// before responding to the relying party's request.
		/// </remarks>
		public static IAnonymousRequest PendingAnonymousRequest {
			get {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				return HttpContext.Current.Session[PendingRequestKey] as IAnonymousRequest;
			}

			set {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				HttpContext.Current.Session[PendingRequestKey] = value;
			}
		}

		/// <summary>
		/// Gets or sets an incoming OpenID request that has not yet been responded to.
		/// </summary>
		/// <remarks>
		/// This request is stored in the ASP.NET Session state, so it will survive across
		/// redirects, postbacks, and transfers.  This allows you to authenticate the user
		/// yourself, and confirm his/her desire to provide data to the relying party site
		/// before responding to the relying party's request.
		/// </remarks>
		public static IHostProcessedRequest PendingRequest {
			get {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest;
			}

			set {
				RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
				RequiresEx.ValidState(HttpContext.Current.Session != null, MessagingStrings.SessionRequired);
				HttpContext.Current.Session[PendingRequestKey] = value;
			}
		}

		/// <summary>
		/// Gets or sets the <see cref="OpenIdProvider"/> instance to use for all instances of this control.
		/// </summary>
		/// <value>The default value is an <see cref="OpenIdProvider"/> instance initialized according to the web.config file.</value>
		public OpenIdProvider Provider {
			get {
				return this.provider.Value;
			}

			set {
				Requires.NotNull(value, "value");
				this.provider = new Lazy<OpenIdProvider>(() => value, LazyThreadSafetyMode.PublicationOnly);
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether or not this control should 
		/// be listening for and responding to incoming OpenID requests.
		/// </summary>
		[Category("Behavior"), DefaultValue(EnabledDefault)]
		public bool Enabled {
			get {
				return ViewState[EnabledViewStateKey] == null ?
				EnabledDefault : (bool)ViewState[EnabledViewStateKey];
			}

			set {
				ViewState[EnabledViewStateKey] = value;
			}
		}

		/// <summary>
		/// Sends the response for the <see cref="PendingAuthenticationRequest" /> and clears the property.
		/// </summary>
		/// <param name="cancellationToken">The cancellation token.</param>
		/// <returns>
		/// The response message.
		/// </returns>
		public Task<HttpResponseMessage> PrepareResponseAsync(CancellationToken cancellationToken = default(CancellationToken)) {
			var pendingRequest = PendingRequest;
			PendingRequest = null;
			return this.Provider.PrepareResponseAsync(pendingRequest, cancellationToken);
		}

		/// <summary>
		/// Checks for incoming OpenID requests, responds to ones it can
		/// respond to without policy checks, and fires events for custom
		/// handling of the ones it cannot decide on automatically.
		/// </summary>
		/// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param>
		protected override void OnLoad(EventArgs e) {
			base.OnLoad(e);

			this.Page.RegisterAsyncTask(new PageAsyncTask(async cancellationToken => {
				// There is the unusual scenario that this control is hosted by
				// an ASP.NET web page that has other UI on it to that the user
				// might see, including controls that cause a postback to occur.
				// We definitely want to ignore postbacks, since any openid messages
				// they contain will be old.
				if (this.Enabled && !this.Page.IsPostBack) {
					// Use the explicitly given state store on this control if there is one.  
					// Then try the configuration file specified one.  Finally, use the default
					// in-memory one that's built into OpenIdProvider.
					// determine what incoming message was received
					IRequest request = await Provider.GetRequestAsync(new HttpRequestWrapper(this.Context.Request), cancellationToken);
					if (request != null) {
						PendingRequest = null;

						// process the incoming message appropriately and send the response
						IAuthenticationRequest idrequest;
						IAnonymousRequest anonRequest;
						if ((idrequest = request as IAuthenticationRequest) != null) {
							PendingAuthenticationRequest = idrequest;
							this.OnAuthenticationChallenge(idrequest);
						} else if ((anonRequest = request as IAnonymousRequest) != null) {
							PendingAnonymousRequest = anonRequest;
							if (!this.OnAnonymousRequest(anonRequest)) {
								// This is a feature not supported by the OP, so
								// go ahead and set disapproved so we can send a response.
								Logger.OpenId.Warn(
									"An incoming anonymous OpenID request message was detected, but the ProviderEndpoint.AnonymousRequest event is not handled, so returning cancellation message to relying party.");
								anonRequest.IsApproved = false;
							}
						}
						if (request.IsResponseReady) {
							PendingAuthenticationRequest = null;
							var response = await Provider.PrepareResponseAsync(request, cancellationToken);
							await response.SendAsync(new HttpContextWrapper(this.Context), cancellationToken);
							this.Context.Response.End();
						}
					}
				}
			}));
		}

		/// <summary>
		/// Fires the <see cref="AuthenticationChallenge"/> event.
		/// </summary>
		/// <param name="request">The request to include in the event args.</param>
		protected virtual void OnAuthenticationChallenge(IAuthenticationRequest request) {
			var authenticationChallenge = this.AuthenticationChallenge;
			if (authenticationChallenge != null) {
				authenticationChallenge(this, new AuthenticationChallengeEventArgs(request));
			}
		}

		/// <summary>
		/// Fires the <see cref="AnonymousRequest"/> event.
		/// </summary>
		/// <param name="request">The request to include in the event args.</param>
		/// <returns><c>true</c> if there were any anonymous request handlers.</returns>
		protected virtual bool OnAnonymousRequest(IAnonymousRequest request) {
			var anonymousRequest = this.AnonymousRequest;
			if (anonymousRequest != null) {
				anonymousRequest(this, new AnonymousRequestEventArgs(request));
				return true;
			} else {
				return false;
			}
		}

		/// <summary>
		/// Creates the default OpenIdProvider to use.
		/// </summary>
		/// <returns>The new instance of OpenIdProvider.</returns>
		private OpenIdProvider CreateProvider() {
			return new OpenIdProvider(OpenIdElement.Configuration.Provider.ApplicationStore.CreateInstance(OpenIdProvider.GetHttpApplicationStore(new HttpContextWrapper(this.Context)), null));
		}
	}
}