aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs')
-rw-r--r--OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs345
1 files changed, 345 insertions, 0 deletions
diff --git a/OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs b/OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs
new file mode 100644
index 0000000..e73961b
--- /dev/null
+++ b/OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs
@@ -0,0 +1,345 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.Collections.Generic;
30using System.Collections.Specialized;
31using System.IO;
32using System.Net;
33using System.Web;
34using DotNetOpenId;
35using DotNetOpenId.Provider;
36using OpenSim.Framework;
37using OpenSim.Framework.Servers;
38using OpenSim.Framework.Servers.HttpServer;
39using OpenSim.Server.Handlers.Base;
40using OpenSim.Services.Interfaces;
41using Nini.Config;
42using OpenMetaverse;
43
44namespace OpenSim.Server.Handlers.Authentication
45{
46 /// <summary>
47 /// Temporary, in-memory store for OpenID associations
48 /// </summary>
49 public class ProviderMemoryStore : IAssociationStore<AssociationRelyingPartyType>
50 {
51 private class AssociationItem
52 {
53 public AssociationRelyingPartyType DistinguishingFactor;
54 public string Handle;
55 public DateTime Expires;
56 public byte[] PrivateData;
57 }
58
59 Dictionary<string, AssociationItem> m_store = new Dictionary<string, AssociationItem>();
60 SortedList<DateTime, AssociationItem> m_sortedStore = new SortedList<DateTime, AssociationItem>();
61 object m_syncRoot = new object();
62
63 #region IAssociationStore<AssociationRelyingPartyType> Members
64
65 public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc)
66 {
67 AssociationItem item = new AssociationItem();
68 item.DistinguishingFactor = distinguishingFactor;
69 item.Handle = assoc.Handle;
70 item.Expires = assoc.Expires.ToLocalTime();
71 item.PrivateData = assoc.SerializePrivateData();
72
73 lock (m_syncRoot)
74 {
75 m_store[item.Handle] = item;
76 m_sortedStore[item.Expires] = item;
77 }
78 }
79
80 public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor)
81 {
82 lock (m_syncRoot)
83 {
84 if (m_sortedStore.Count > 0)
85 {
86 AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1];
87 return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
88 }
89 else
90 {
91 return null;
92 }
93 }
94 }
95
96 public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
97 {
98 AssociationItem item;
99 bool success = false;
100 lock (m_syncRoot)
101 success = m_store.TryGetValue(handle, out item);
102
103 if (success)
104 return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
105 else
106 return null;
107 }
108
109 public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
110 {
111 lock (m_syncRoot)
112 {
113 for (int i = 0; i < m_sortedStore.Values.Count; i++)
114 {
115 AssociationItem item = m_sortedStore.Values[i];
116 if (item.Handle == handle)
117 {
118 m_sortedStore.RemoveAt(i);
119 break;
120 }
121 }
122
123 return m_store.Remove(handle);
124 }
125 }
126
127 public void ClearExpiredAssociations()
128 {
129 lock (m_syncRoot)
130 {
131 List<AssociationItem> itemsCopy = new List<AssociationItem>(m_sortedStore.Values);
132 DateTime now = DateTime.Now;
133
134 for (int i = 0; i < itemsCopy.Count; i++)
135 {
136 AssociationItem item = itemsCopy[i];
137
138 if (item.Expires <= now)
139 {
140 m_sortedStore.RemoveAt(i);
141 m_store.Remove(item.Handle);
142 }
143 }
144 }
145 }
146
147 #endregion
148 }
149
150 public class OpenIdStreamHandler : IStreamHandler
151 {
152 #region HTML
153
154 /// <summary>Login form used to authenticate OpenID requests</summary>
155 const string LOGIN_PAGE =
156@"<html>
157<head><title>OpenSim OpenID Login</title></head>
158<body>
159<h3>OpenSim Login</h3>
160<form method=""post"">
161<label for=""first"">First Name:</label> <input readonly type=""text"" name=""first"" id=""first"" value=""{0}""/>
162<label for=""last"">Last Name:</label> <input readonly type=""text"" name=""last"" id=""last"" value=""{1}""/>
163<label for=""pass"">Password:</label> <input type=""password"" name=""pass"" id=""pass""/>
164<input type=""submit"" value=""Login"">
165</form>
166</body>
167</html>";
168
169 /// <summary>Page shown for a valid OpenID identity</summary>
170 const string OPENID_PAGE =
171@"<html>
172<head>
173<title>{2} {3}</title>
174<link rel=""openid2.provider openid.server"" href=""{0}://{1}/openid/server/""/>
175</head>
176<body>OpenID identifier for {2} {3}</body>
177</html>
178";
179
180 /// <summary>Page shown for an invalid OpenID identity</summary>
181 const string INVALID_OPENID_PAGE =
182@"<html><head><title>Identity not found</title></head>
183<body>Invalid OpenID identity</body></html>";
184
185 /// <summary>Page shown if the OpenID endpoint is requested directly</summary>
186 const string ENDPOINT_PAGE =
187@"<html><head><title>OpenID Endpoint</title></head><body>
188This is an OpenID server endpoint, not a human-readable resource.
189For more information, see <a href='http://openid.net/'>http://openid.net/</a>.
190</body></html>";
191
192 #endregion HTML
193
194 public string ContentType { get { return m_contentType; } }
195 public string HttpMethod { get { return m_httpMethod; } }
196 public string Path { get { return m_path; } }
197
198 string m_contentType;
199 string m_httpMethod;
200 string m_path;
201 IAuthenticationService m_authenticationService;
202 IUserAccountService m_userAccountService;
203 ProviderMemoryStore m_openidStore = new ProviderMemoryStore();
204
205 /// <summary>
206 /// Constructor
207 /// </summary>
208 public OpenIdStreamHandler(string httpMethod, string path, IUserAccountService userService, IAuthenticationService authService)
209 {
210 m_authenticationService = authService;
211 m_userAccountService = userService;
212 m_httpMethod = httpMethod;
213 m_path = path;
214
215 m_contentType = "text/html";
216 }
217
218 /// <summary>
219 /// Handles all GET and POST requests for OpenID identifier pages and endpoint
220 /// server communication
221 /// </summary>
222 public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
223 {
224 Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath));
225
226 // Defult to returning HTML content
227 m_contentType = "text/html";
228
229 try
230 {
231 NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd());
232 NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query);
233 NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery);
234
235 OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery);
236
237 if (provider.Request != null)
238 {
239 if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest)
240 {
241 IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request;
242 string[] passwordValues = postQuery.GetValues("pass");
243
244 UserAccount account;
245 if (TryGetAccount(new Uri(authRequest.ClaimedIdentifier.ToString()), out account))
246 {
247 // Check for form POST data
248 if (passwordValues != null && passwordValues.Length == 1)
249 {
250 if (account != null &&
251 (m_authenticationService.Authenticate(account.PrincipalID, passwordValues[0], 30) != string.Empty))
252 authRequest.IsAuthenticated = true;
253 else
254 authRequest.IsAuthenticated = false;
255 }
256 else
257 {
258 // Authentication was requested, send the client a login form
259 using (StreamWriter writer = new StreamWriter(response))
260 writer.Write(String.Format(LOGIN_PAGE, account.FirstName, account.LastName));
261 return;
262 }
263 }
264 else
265 {
266 // Cannot find an avatar matching the claimed identifier
267 authRequest.IsAuthenticated = false;
268 }
269 }
270
271 // Add OpenID headers to the response
272 foreach (string key in provider.Request.Response.Headers.Keys)
273 httpResponse.AddHeader(key, provider.Request.Response.Headers[key]);
274
275 string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type");
276 if (contentTypeValues != null && contentTypeValues.Length == 1)
277 m_contentType = contentTypeValues[0];
278
279 // Set the response code and document body based on the OpenID result
280 httpResponse.StatusCode = (int)provider.Request.Response.Code;
281 response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length);
282 response.Close();
283 }
284 else if (httpRequest.Url.AbsolutePath.Contains("/openid/server"))
285 {
286 // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page
287 using (StreamWriter writer = new StreamWriter(response))
288 writer.Write(ENDPOINT_PAGE);
289 }
290 else
291 {
292 // Try and lookup this avatar
293 UserAccount account;
294 if (TryGetAccount(httpRequest.Url, out account))
295 {
296 using (StreamWriter writer = new StreamWriter(response))
297 {
298 // TODO: Print out a full profile page for this avatar
299 writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme,
300 httpRequest.Url.Authority, account.FirstName, account.LastName));
301 }
302 }
303 else
304 {
305 // Couldn't parse an avatar name, or couldn't find the avatar in the user server
306 using (StreamWriter writer = new StreamWriter(response))
307 writer.Write(INVALID_OPENID_PAGE);
308 }
309 }
310 }
311 catch (Exception ex)
312 {
313 httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
314 using (StreamWriter writer = new StreamWriter(response))
315 writer.Write(ex.Message);
316 }
317 }
318
319 /// <summary>
320 /// Parse a URL with a relative path of the form /users/First_Last and try to
321 /// retrieve the profile matching that avatar name
322 /// </summary>
323 /// <param name="requestUrl">URL to parse for an avatar name</param>
324 /// <param name="profile">Profile data for the avatar</param>
325 /// <returns>True if the parse and lookup were successful, otherwise false</returns>
326 bool TryGetAccount(Uri requestUrl, out UserAccount account)
327 {
328 if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/")
329 {
330 // Parse the avatar name from the path
331 string username = requestUrl.Segments[requestUrl.Segments.Length - 1];
332 string[] name = username.Split('_');
333
334 if (name.Length == 2)
335 {
336 account = m_userAccountService.GetUserAccount(UUID.Zero, name[0], name[1]);
337 return (account != null);
338 }
339 }
340
341 account = null;
342 return false;
343 }
344 }
345}