From b0bbe861cd0f3eb06de73a371ab961428c549c69 Mon Sep 17 00:00:00 2001 From: Diva Canto Date: Sun, 10 Jan 2010 17:15:02 -0800 Subject: Moved OpenId authentication from user server to Server.Handlers.Authentication. --- .../Handlers/Authentication/OpenIdServerHandler.cs | 345 +++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs (limited to 'OpenSim/Server/Handlers/Authentication/OpenIdServerHandler.cs') 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 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Web; +using DotNetOpenId; +using DotNetOpenId.Provider; +using OpenSim.Framework; +using OpenSim.Framework.Servers; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Server.Handlers.Base; +using OpenSim.Services.Interfaces; +using Nini.Config; +using OpenMetaverse; + +namespace OpenSim.Server.Handlers.Authentication +{ + /// + /// Temporary, in-memory store for OpenID associations + /// + public class ProviderMemoryStore : IAssociationStore + { + private class AssociationItem + { + public AssociationRelyingPartyType DistinguishingFactor; + public string Handle; + public DateTime Expires; + public byte[] PrivateData; + } + + Dictionary m_store = new Dictionary(); + SortedList m_sortedStore = new SortedList(); + object m_syncRoot = new object(); + + #region IAssociationStore Members + + public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc) + { + AssociationItem item = new AssociationItem(); + item.DistinguishingFactor = distinguishingFactor; + item.Handle = assoc.Handle; + item.Expires = assoc.Expires.ToLocalTime(); + item.PrivateData = assoc.SerializePrivateData(); + + lock (m_syncRoot) + { + m_store[item.Handle] = item; + m_sortedStore[item.Expires] = item; + } + } + + public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor) + { + lock (m_syncRoot) + { + if (m_sortedStore.Count > 0) + { + AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1]; + return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData); + } + else + { + return null; + } + } + } + + public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle) + { + AssociationItem item; + bool success = false; + lock (m_syncRoot) + success = m_store.TryGetValue(handle, out item); + + if (success) + return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData); + else + return null; + } + + public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle) + { + lock (m_syncRoot) + { + for (int i = 0; i < m_sortedStore.Values.Count; i++) + { + AssociationItem item = m_sortedStore.Values[i]; + if (item.Handle == handle) + { + m_sortedStore.RemoveAt(i); + break; + } + } + + return m_store.Remove(handle); + } + } + + public void ClearExpiredAssociations() + { + lock (m_syncRoot) + { + List itemsCopy = new List(m_sortedStore.Values); + DateTime now = DateTime.Now; + + for (int i = 0; i < itemsCopy.Count; i++) + { + AssociationItem item = itemsCopy[i]; + + if (item.Expires <= now) + { + m_sortedStore.RemoveAt(i); + m_store.Remove(item.Handle); + } + } + } + } + + #endregion + } + + public class OpenIdStreamHandler : IStreamHandler + { + #region HTML + + /// Login form used to authenticate OpenID requests + const string LOGIN_PAGE = +@" +OpenSim OpenID Login + +

OpenSim Login

+
+ + + + +
+ +"; + + /// Page shown for a valid OpenID identity + const string OPENID_PAGE = +@" + +{2} {3} + + +OpenID identifier for {2} {3} + +"; + + /// Page shown for an invalid OpenID identity + const string INVALID_OPENID_PAGE = +@"Identity not found +Invalid OpenID identity"; + + /// Page shown if the OpenID endpoint is requested directly + const string ENDPOINT_PAGE = +@"OpenID Endpoint +This is an OpenID server endpoint, not a human-readable resource. +For more information, see http://openid.net/. +"; + + #endregion HTML + + public string ContentType { get { return m_contentType; } } + public string HttpMethod { get { return m_httpMethod; } } + public string Path { get { return m_path; } } + + string m_contentType; + string m_httpMethod; + string m_path; + IAuthenticationService m_authenticationService; + IUserAccountService m_userAccountService; + ProviderMemoryStore m_openidStore = new ProviderMemoryStore(); + + /// + /// Constructor + /// + public OpenIdStreamHandler(string httpMethod, string path, IUserAccountService userService, IAuthenticationService authService) + { + m_authenticationService = authService; + m_userAccountService = userService; + m_httpMethod = httpMethod; + m_path = path; + + m_contentType = "text/html"; + } + + /// + /// Handles all GET and POST requests for OpenID identifier pages and endpoint + /// server communication + /// + public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse) + { + Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath)); + + // Defult to returning HTML content + m_contentType = "text/html"; + + try + { + NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd()); + NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query); + NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery); + + OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery); + + if (provider.Request != null) + { + if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest) + { + IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request; + string[] passwordValues = postQuery.GetValues("pass"); + + UserAccount account; + if (TryGetAccount(new Uri(authRequest.ClaimedIdentifier.ToString()), out account)) + { + // Check for form POST data + if (passwordValues != null && passwordValues.Length == 1) + { + if (account != null && + (m_authenticationService.Authenticate(account.PrincipalID, passwordValues[0], 30) != string.Empty)) + authRequest.IsAuthenticated = true; + else + authRequest.IsAuthenticated = false; + } + else + { + // Authentication was requested, send the client a login form + using (StreamWriter writer = new StreamWriter(response)) + writer.Write(String.Format(LOGIN_PAGE, account.FirstName, account.LastName)); + return; + } + } + else + { + // Cannot find an avatar matching the claimed identifier + authRequest.IsAuthenticated = false; + } + } + + // Add OpenID headers to the response + foreach (string key in provider.Request.Response.Headers.Keys) + httpResponse.AddHeader(key, provider.Request.Response.Headers[key]); + + string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type"); + if (contentTypeValues != null && contentTypeValues.Length == 1) + m_contentType = contentTypeValues[0]; + + // Set the response code and document body based on the OpenID result + httpResponse.StatusCode = (int)provider.Request.Response.Code; + response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length); + response.Close(); + } + else if (httpRequest.Url.AbsolutePath.Contains("/openid/server")) + { + // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page + using (StreamWriter writer = new StreamWriter(response)) + writer.Write(ENDPOINT_PAGE); + } + else + { + // Try and lookup this avatar + UserAccount account; + if (TryGetAccount(httpRequest.Url, out account)) + { + using (StreamWriter writer = new StreamWriter(response)) + { + // TODO: Print out a full profile page for this avatar + writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme, + httpRequest.Url.Authority, account.FirstName, account.LastName)); + } + } + else + { + // Couldn't parse an avatar name, or couldn't find the avatar in the user server + using (StreamWriter writer = new StreamWriter(response)) + writer.Write(INVALID_OPENID_PAGE); + } + } + } + catch (Exception ex) + { + httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError; + using (StreamWriter writer = new StreamWriter(response)) + writer.Write(ex.Message); + } + } + + /// + /// Parse a URL with a relative path of the form /users/First_Last and try to + /// retrieve the profile matching that avatar name + /// + /// URL to parse for an avatar name + /// Profile data for the avatar + /// True if the parse and lookup were successful, otherwise false + bool TryGetAccount(Uri requestUrl, out UserAccount account) + { + if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/") + { + // Parse the avatar name from the path + string username = requestUrl.Segments[requestUrl.Segments.Length - 1]; + string[] name = username.Split('_'); + + if (name.Length == 2) + { + account = m_userAccountService.GetUserAccount(UUID.Zero, name[0], name[1]); + return (account != null); + } + } + + account = null; + return false; + } + } +} -- cgit v1.1