Index: xwiki-core/pom.xml =================================================================== --- xwiki-core/pom.xml (revision 11817) +++ xwiki-core/pom.xml (working copy) @@ -332,6 +332,13 @@ 2.0 + + + org.openid4java + openid4java + 0.9.3 + + dom4j Index: xwiki-core/src/main/java/com/xpn/xwiki/plugin/openid/OpenIdHelper.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/plugin/openid/OpenIdHelper.java (revision 0) +++ xwiki-core/src/main/java/com/xpn/xwiki/plugin/openid/OpenIdHelper.java (revision 0) @@ -0,0 +1,278 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ +package com.xpn.xwiki.plugin.openid; + +import java.util.HashMap; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openid4java.consumer.ConsumerException; +import org.openid4java.consumer.ConsumerManager; + +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; +import com.xpn.xwiki.objects.classes.BaseClass; +import com.xpn.xwiki.store.query.Query; +import com.xpn.xwiki.store.query.QueryManager; + +/** + * OpenID helper class. This singleton class contains various helper methods used for OpenID related tasks. + * + * @author Markus Lanthaler + */ +public class OpenIdHelper +{ + private static final Log log = LogFactory.getLog(OpenIdHelper.class); + + private static OpenIdHelper instance; + + private ConsumerManager manager; + + /** + * The constructor instantiates a ConsumerManager object. + * + * @throws ConsumerException + */ + private OpenIdHelper() throws ConsumerException + { + manager = new ConsumerManager(); + } + + /** + * Gets the unique consumer manager class. + * + * @return the unique ConsumerManager instance + * @throws ConsumerException + */ + public static ConsumerManager getConsumerManager() throws ConsumerException + { + if (instance == null) + instance = new OpenIdHelper(); + + return instance.manager; + } + + /** + * Converts an OpenID identifier to an user name which can be used as a XWiki document name. This method + * doesn't check if that user already exists! + * + * @param openid_identifier the OpenID identifier to convert + * @param context the context + * @return the converted OpenID identifier + */ + public static String openIdIdentifierToUsername(String openid_identifier, XWikiContext context) + { + return "OpenID-" + context.getWiki().clearName(openid_identifier, true, true, context) + "-" + + openid_identifier.hashCode(); + } + + /** + * Finds the user belonging to a specific OpenID identifier. + * + * @param openid_identifier the OpenID identifier to search for + * @param context the context + * @return the full document name for the user belonging to the OpenID identifier or null if the + * OpenID identifier was not found. + */ + public static String findUser(String openid_identifier, XWikiContext context) throws XWikiException + { + XWiki xwiki = context.getWiki(); + + QueryManager qm = xwiki.getStore().getQueryManager(); + Query search_user = qm.getNamedQuery("getUserDocByOpenIdIdentifier"); + + if (search_user == null) + throw new RuntimeException("Named query 'getUserDocByOpenIdIdentifier' was not found!"); + + search_user.bindValue("identifier", openid_identifier); + + List found_users = search_user.setLimit(1).execute(); + if (found_users.size() > 0) { + if (log.isDebugEnabled()) { + log.debug("OpenID " + openid_identifier + " already registered."); + } + return found_users.get(0); + } + + return null; + } + + /** + * Creates a new OpenID user with a random password. The user doesn't need to know the password - in fact he even + * doesn't need to know of its existence. It's just used to use the {@link PersistentLoginManager} and the + * Authenticator as they are implemented at the moment. + * + * @param openid_identifier the OpenID identifier + * @param firstname users first name + * @param lastname users last name + * @param email users email address + * @param context the context + * @return a code which describes the success or failure of the method + * @throws XWikiException + */ + public static int createUser(String openid_identifier, String firstname, String lastname, String email, + XWikiContext context) throws XWikiException + { + XWiki xwiki = context.getWiki(); + String xwikiname = openIdIdentifierToUsername(openid_identifier, context); + + // Generate a unique document name for the new user + XWikiDocument userdoc = xwiki.getDocument("XWiki." + xwikiname, context); + while (userdoc.isNew() == false) { + userdoc = xwiki.getDocument("XWiki." + xwikiname + "-" + xwiki.generateRandomString(5), context); + } + + if (userdoc.isNew()) { + if (log.isDebugEnabled()) { + log.debug("Creating user for OpenID " + openid_identifier); + } + + String content = "#includeForm(\"XWiki.XWikiUserSheet\")"; + String parent = "XWiki.XWikiUsers"; + HashMap map = new HashMap(); + map.put("active", "1"); + if (firstname == null || firstname.trim().isEmpty()) + map.put("first_name", openid_identifier); + else + map.put("first_name", firstname); + + if (lastname != null) + map.put("last_name", lastname); + + if (email != null) + map.put("email", email); + + String password = xwiki.generateRandomString(255); + map.put("password", password); + + int result; + if ((result = xwiki.createUser(xwikiname, map, parent, content, "edit", context)) == 1) { + // change the return value to output a different message for OpenID users + result = 3; + + userdoc = xwiki.getDocument("XWiki." + xwikiname, context); + + if (attachOpenIdToUser(userdoc, openid_identifier, context) == false) { + if (log.isDebugEnabled()) { + log.debug("Deleting previously created document for OpenID user " + openid_identifier + + ". OpenID identifier is already in use."); + } + xwiki.deleteDocument(userdoc, false, context); + result = -13; + } + } + return result; + } else { + if (log.isDebugEnabled()) { + log.debug("User page already exists for OpenID " + openid_identifier); + } + return -3; + } + } + + /** + * Attaches an OpenID identifier to a document. The method assures that the identifier is unique. If it is already + * used, the method fails and returns false. The document is automatically saved by this + * method. + * + * @param doc document to which the OpenID identifier should be attached + * @param openid_identifier the OpenID identifier to attach + * @param context the context + * @return true if attaching the OpenID identifier was successful, otherwise false. + * @throws XWikiException + */ + public static synchronized boolean attachOpenIdToUser(XWikiDocument doc, String openid_identifier, + XWikiContext context) throws XWikiException + { + XWiki xwiki = context.getWiki(); + + if (findUser(openid_identifier, context) != null) { + if (log.isDebugEnabled()) { + log.debug("OpenID " + openid_identifier + " is already registered"); + } + return false; + } + + BaseClass baseclass = getOpenIdIdentifierClass(context); + BaseObject newobject = (BaseObject) baseclass.newObject(context); + newobject.setName(doc.getFullName()); + newobject.setStringValue("identifier", openid_identifier); + + doc.addObject(baseclass.getName(), newobject); + xwiki.saveDocument(doc, context.getMessageTool().get("core.comment.addedOpenIdIdentifier"), context); + + return true; + } + + /** + * Retrieves the password for an OpenID identifier. Since for all OpenID users a random password which the user + * doesn't know is created automatically, we need some way to retrieve it. This method does exactly that. + * + * @param openid_identifier the OpenID identifier + * @param context the context + * @return the internal used password for the OpenID user or null if the user was not found + */ + public static String getOpenIdUserPassword(String openid_identifier, XWikiContext context) throws XWikiException + { + String xwikiname = findUser(openid_identifier, context); + + XWikiDocument doc = context.getWiki().getDocument(xwikiname, context); + // We only allow empty password from users having a XWikiUsers object. + if (doc.getObject("XWiki.OpenIdIdentifier") != null && doc.getObject("XWiki.XWikiUsers") != null) { + return doc.getStringValue("XWiki.XWikiUsers", "password"); + } + + return null; + } + + /** + * Gets the OpenIDIdentifer class. Verifies if the XWiki.OpenIdIdentifier page exists and that it + * contains all the required configuration properties to make the OpenID feature work properly. If some properties + * are missing they are created and saved in the database. + * + * @param context the XWiki Context + * @return the OpenIdIdentifier Base Class object containing the properties + * @throws XWikiException if an error happens while saving + */ + public static BaseClass getOpenIdIdentifierClass(XWikiContext context) throws XWikiException + { + XWiki xwiki = context.getWiki(); + XWikiDocument doc; + boolean needs_update = false; + + doc = xwiki.getDocument("XWiki.OpenIdIdentifier", context); + + BaseClass bclass = doc.getxWikiClass(); + bclass.setName("XWiki.OpenIdIdentifier"); + + needs_update |= bclass.addTextField("identifier", "Identifier", 2048); + needs_update |= bclass.addTextField("password", "Password", 255); + + if (needs_update) + xwiki.saveDocument(doc, context); + + return bclass; + } +} Index: xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/MyFormAuthenticator.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/MyFormAuthenticator.java (revision 11817) +++ xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/MyFormAuthenticator.java (working copy) @@ -23,6 +23,7 @@ import java.io.IOException; import java.security.Principal; +import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -30,6 +31,14 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.velocity.VelocityContext; +import org.openid4java.OpenIDException; +import org.openid4java.consumer.ConsumerManager; +import org.openid4java.consumer.VerificationResult; +import org.openid4java.discovery.DiscoveryInformation; +import org.openid4java.discovery.Identifier; +import org.openid4java.message.AuthRequest; +import org.openid4java.message.ParameterList; import org.securityfilter.authenticator.Authenticator; import org.securityfilter.authenticator.FormAuthenticator; import org.securityfilter.filter.SecurityRequestWrapper; @@ -37,7 +46,9 @@ import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.plugin.openid.OpenIdHelper; import com.xpn.xwiki.web.SavedRequestRestorerFilter; +import com.xpn.xwiki.web.XWikiServletURLFactory; public class MyFormAuthenticator extends FormAuthenticator implements Authenticator, XWikiAuthenticator { @@ -116,7 +127,7 @@ } } catch (Exception e) { // in case of exception we continue on Form Auth. - // we don't want this to interfere with the most common behavior + // we don't want this to interfere with the most common behaviour } // process any persistent login information, if user is not already logged in, @@ -139,13 +150,27 @@ } } - // process login form submittal + // process login form data if ((this.loginSubmitPattern != null) && request.getMatchableURL().endsWith(this.loginSubmitPattern)) { - String username = convertUsername(request.getParameter(FORM_USERNAME), context); - String password = request.getParameter(FORM_PASSWORD); - String rememberme = request.getParameter(FORM_REMEMBERME); - rememberme = (rememberme == null) ? "false" : rememberme; - return processLogin(username, password, rememberme, request, response, context); + if (request.getParameter("authentication_method") != null + && request.getParameter("authentication_method").equalsIgnoreCase("openid")) { + // OpenID login + String openid_identifier = request.getParameter("openid_identifier"); + String rememberme = request.getParameter(FORM_REMEMBERME); + rememberme = (rememberme == null) ? "false" : rememberme; + return processOpenIdLogin(openid_identifier, rememberme, request, response, context); + } else if (request.getParameter("authentication_method") != null + && request.getParameter("authentication_method").equalsIgnoreCase("useraccount")) { + // Normal user account login + String username = convertUsername(request.getParameter(FORM_USERNAME), context); + String password = request.getParameter(FORM_PASSWORD); + String rememberme = request.getParameter(FORM_REMEMBERME); + rememberme = (rememberme == null) ? "false" : rememberme; + return processLogin(username, password, rememberme, request, response, context); + } else if (request.getParameter("openid.mode") != null) { + // OpenID: OP response + return processOpenIdLoginResponse(request, response, context); + } } return false; } @@ -155,6 +180,9 @@ * abort further processing after the method completes (for example, if a redirect was sent as part of the login * processing). * + * @param username + * @param password + * @param rememberme * @param request * @param response * @return true if the filter should return after this method ends, false otherwise @@ -169,7 +197,8 @@ log.info("User " + principal.getName() + " has been logged-in"); } - // invalidate old session if the user was already authenticated, and they logged in as a different user + // invalidate old session if the user was already authenticated, and they logged in as a + // different user if (request.getUserPrincipal() != null && !username.equals(request.getRemoteUser())) { request.getSession().invalidate(); } @@ -190,7 +219,8 @@ Boolean bAjax = (Boolean) context.get("ajax"); if ((bAjax == null) || (!bAjax.booleanValue())) { String continueToURL = getContinueToURL(request); - // This is the url that the user was initially accessing before being prompted for login. + // This is the url that the user was initially accessing before being prompted for + // login. response.sendRedirect(response.encodeRedirectURL(continueToURL)); } } else { @@ -215,6 +245,173 @@ } /** + * Processes an OpenID login. It redirects the user as part of a normal OpenID login process to the OpenID provider. + * The response is handled by {@link MyFormAuthenticator#processOpenIdLoginResponse processOpenIdLoginResponse}. + * Returns true if SecurityFilter should abort further processing after the method completes (for example, if a + * redirect was sent as part of the login processing which is the normal behaviour). + * + * @param openid_identifier the OpenID identifier + * @param rememberme "true" if the login should be persistent, null or + * "false" otherwise. + * @param request the request object + * @param response the response object + * @return true if the filter should return after this method ends, false otherwise + */ + public boolean processOpenIdLogin(String openid_identifier, String rememberme, SecurityRequestWrapper request, + HttpServletResponse response, XWikiContext context) throws Exception + { + if (openid_identifier == null || openid_identifier.trim().equals("")) { + context.put("message", "noopenid"); + return false; + } + + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + String return_to_url = + context.getWiki().getExternalURL("XWikiLogin", "loginsubmit", "rememberme=" + rememberme, context); + + List discoveries = manager.discover(openid_identifier); + DiscoveryInformation discovered = manager.associate(discoveries); + + // store the discovery information in the user's session + request.getSession().setAttribute("openid-discovery", discovered); + + AuthRequest auth_request = manager.authenticate(discovered, return_to_url); + + // set the realm + auth_request.setRealm(((XWikiServletURLFactory) context.getURLFactory()).getServerURL(context).toString() + + ((XWikiServletURLFactory) context.getURLFactory()).getContextPath()); + + if (log.isInfoEnabled()) { + log.info("Redirecting user to OP (OpenID identifier: " + openid_identifier + ")"); + } + + if (discovered.isVersion2()) { + // OpenID 2.0 supports HTML FORM Redirection which allows payloads >2048 bytes + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + vcontext.put("op_endpoint", auth_request.getDestinationUrl(false)); + vcontext.put("openid_parameters", auth_request.getParameterMap()); + + String redirect_form = context.getWiki().parseTemplate("openid_form_redirect.vm", context); + + response.getOutputStream().print(redirect_form); + + // Close the output stream - otherwise the login form documented is also written to it + response.getOutputStream().close(); + } else { + // The only method supported in OpenID 1.x is a HTTP-redirect (GET) to the OpenID Provider endpoint (the + // redirect-URL usually limited ~2048 bytes) + response.sendRedirect(auth_request.getDestinationUrl(true)); + } + } catch (OpenIDException e) { + if (log.isInfoEnabled()) { + log.info("OpenID discovery failed: " + e.getMessage()); + } + + // present error to the user + context.put("message", "loginfailed"); + } + + return true; + } + + /** + * Processes the response of an OpenID provider to complete the login process. Checks the response of the OP and in + * case of success it logs in the user. Otherwise an error message is put into the context and shown to the user + * afterwards. + * + * @param request the request object + * @param response the response object + * @param context the context + * @return true if the filter should return after this method ends, false otherwise + */ + public boolean processOpenIdLoginResponse(SecurityRequestWrapper request, HttpServletResponse response, + XWikiContext context) throws Exception + { + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + // extract the parameters from the authentication response which come in as a HTTP request from the OpenID + // provider + ParameterList openid_response = new ParameterList(request.getParameterMap()); + + // retrieve the previously stored discovery information + DiscoveryInformation discovered = + (DiscoveryInformation) request.getSession().getAttribute("openid-discovery"); + + // verify the response + StringBuffer receivingURL = request.getRequestURL(); + String queryString = request.getQueryString(); + if (queryString != null && queryString.length() > 0) + receivingURL.append("?").append(request.getQueryString()); + + VerificationResult verification = manager.verify(receivingURL.toString(), openid_response, discovered); + Identifier verified = verification.getVerifiedId(); + + if (verified != null) { + String username = OpenIdHelper.findUser(verified.getIdentifier(), context); + + if (username == null) { + // no user was found for this OpenID identifier + if (log.isInfoEnabled()) { + log.info("No user for OpenID " + verified.getIdentifier() + " found."); + } + + context.put("message", "openid_not_associated"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return true; + } + + // The current authentication mechanisms is implemented in a very restrictive manner, we have to + // retrieve the user password (which is generated randomly during registration for OpenID users) and use + // it in order to authenticate the user. + String password = OpenIdHelper.getOpenIdUserPassword(verified.getIdentifier(), context); + Principal principal = authenticate(username, password, context); + if (principal != null) { + // invalidate old session if the user was already authenticated and logged in as a different user + if (request.getUserPrincipal() != null) { + request.getSession().invalidate(); + } + + // manage persistent login info if persistent login management is enabled + String rememberme = request.getParameter(FORM_REMEMBERME); + rememberme = (rememberme == null) ? "false" : rememberme; + + if (this.persistentLoginManager != null) { + if (rememberme != null) { + this.persistentLoginManager.rememberLogin(request, response, username, password); + } else { + this.persistentLoginManager.forgetLogin(request, response); + } + } + + request.setUserPrincipal(principal); + + String continueToURL = getContinueToURL(request); + response.sendRedirect(response.encodeRedirectURL(continueToURL)); + } + } else { + // authentication failed, show and log error message + if (openid_response.getParameter("openid.mode") != null + && openid_response.getParameter("openid.mode").getValue().equals("cancel")) { + context.put("message", "openidlogin_cancelled"); + } else { + if (log.isInfoEnabled() && openid_response.getParameter("error") != null) { + log.info("OpenID login failed (error: " + + openid_response.getParameter("openid.error").getValue() + ")"); + } + context.put("message", "loginfailed"); + } + } + } catch (OpenIDException e) { + context.put("message", "loginfailed"); + } + + return true; + } + + /** * FormAuthenticator has a special case where the user should be sent to a default page if the user spontaneously * submits a login request. * Index: xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/XWikiAuthServiceImpl.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/XWikiAuthServiceImpl.java (revision 11817) +++ xwiki-core/src/main/java/com/xpn/xwiki/user/impl/xwiki/XWikiAuthServiceImpl.java (working copy) @@ -242,7 +242,7 @@ String sql = "delete from XWikiLock as lock where lock.userName=:userName"; Query query = session.createQuery(sql); - String localName = user.getName().substring(user.getName().indexOf(":") + 1); + String localName = user.getName().substring(user.getName().indexOf(":") + 1); query.setString("userName", localName); query.executeUpdate(); } catch (Exception e) { @@ -475,18 +475,23 @@ return user; } - protected boolean checkPassword(String username, String password, XWikiContext context) - throws XWikiException + protected boolean checkPassword(String username, String password, XWikiContext context) throws XWikiException { try { boolean result = false; XWikiDocument doc = context.getWiki().getDocument(username, context); - // We only allow empty password from users having a XWikiUsers object. - if (doc.getObject("XWiki.XWikiUsers") != null) { + if (doc.getObject("XWiki.OpenIdIdentifier") != null) { + // For users having an OpenID the password doesn't need to be adjusted because it is set to the current + // value during the login process String passwd = doc.getStringValue("XWiki.XWikiUsers", "password"); + result = (password.equals(passwd)); + } + if (result == false && doc.getObject("XWiki.XWikiUsers") != null) { + // We only allow empty password from users having a XWikiUsers object. + String passwd = doc.getStringValue("XWiki.XWikiUsers", "password"); password = - ((PasswordClass) context.getWiki().getClass("XWiki.XWikiUsers", context) - .getField("password")).getEquivalentPassword(passwd, password); + ((PasswordClass) context.getWiki().getClass("XWiki.XWikiUsers", context).getField("password")) + .getEquivalentPassword(passwd, password); result = (password.equals(passwd)); } Index: xwiki-core/src/main/java/com/xpn/xwiki/web/AttachOpenIdAction.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/web/AttachOpenIdAction.java (revision 0) +++ xwiki-core/src/main/java/com/xpn/xwiki/web/AttachOpenIdAction.java (revision 0) @@ -0,0 +1,286 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ +package com.xpn.xwiki.web; + +import java.util.List; + +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; +import com.xpn.xwiki.objects.classes.PasswordClass; +import com.xpn.xwiki.plugin.openid.OpenIdHelper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.velocity.VelocityContext; + +import org.openid4java.consumer.ConsumerManager; +import org.openid4java.consumer.VerificationResult; +import org.openid4java.discovery.Identifier; +import org.openid4java.discovery.DiscoveryInformation; +import org.openid4java.message.ax.AxMessage; +import org.openid4java.message.ax.FetchRequest; +import org.openid4java.message.ax.FetchResponse; +import org.openid4java.message.sreg.SRegMessage; +import org.openid4java.message.sreg.SRegRequest; +import org.openid4java.message.sreg.SRegResponse; +import org.openid4java.message.*; +import org.openid4java.OpenIDException; + +/** + * Action used to attach an OpenID to already existing users. + * + * @author Markus Lanthaler + */ +public class AttachOpenIdAction extends XWikiAction +{ + private static final Log log = LogFactory.getLog(AttachOpenIdAction.class); + + private String template = "attach_openid"; + + public boolean action(XWikiContext context) throws XWikiException + { + // TODO Make sure that the user is signed in! + + XWikiRequest request = context.getRequest(); + XWikiResponse response = context.getResponse(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + + template = "attach_openid"; + + String attach_openid = request.getParameter("attach_openid"); + if ((attach_openid != null) && (attach_openid.equals("discover-openid"))) { + if (discoverOpenID(context)) + return false; + } else if ((attach_openid != null) && (attach_openid.equals("confirm"))) { + vcontext.put("status", new Integer(confirmAttachingOpenId(context))); + } else if ((attach_openid != null) && (attach_openid.equals("attach-openid"))) { + vcontext.put("status", new Integer(attachOpenID(context))); + } else { + // Check if the user has already attached an OpenID + XWikiDocument user_doc; + user_doc = context.getWiki().getDocument(context.getXWikiUser().getUser(), context); + + if (user_doc.getObject("XWiki.OpenIdIdentifier") != null) { + vcontext.put("status", new Integer(-3)); // Error! User needs to delete his OpenID first + } + } + + String redirect = Utils.getRedirect(request, null); + if (redirect == null) + return true; + else { + sendRedirect(response, redirect); + return false; + } + } + + public String render(XWikiContext context) throws XWikiException + { + return template; + } + + /** + * Starts the process of attaching an OpenID to an existing user. The OpenID provider belonging to the entered + * OpenID identifier is searched and the user is redirected to it to authenticate there. This processed is used to + * assure that the entered OpenID is valid and in possession of that user. If discovery fails, an error message is + * shown. + * + * @param context + * @return returns true if a redirect was sent, otherwise false. + */ + protected boolean discoverOpenID(XWikiContext context) + { + XWikiRequest request = context.getRequest(); + XWikiResponse response = context.getResponse(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + + // Check for empty OpenID identifier + String openid_identifier = request.getParameter("openid_identifier"); + if (openid_identifier == null || openid_identifier.equals("")) { + vcontext.put("status", new Integer(-14)); + return false; + } + + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + String return_to_url = + context.getWiki().getExternalURL("AttachOpenID", "attachopenid", "attach_openid=confirm", + context); + + // perform discovery on the user-supplied identifier + List discoveries = manager.discover(openid_identifier); + + // attempt to associate with the OpenID provider and retrieve one service endpoint for authentication + DiscoveryInformation discovered = manager.associate(discoveries); + request.getSession().setAttribute("openid-discovery", discovered); + + // obtain a AuthRequest message to be sent to the OpenID provider + AuthRequest auth_request = manager.authenticate(discovered, return_to_url); + + // set the realm + auth_request.setRealm(((XWikiServletURLFactory) context.getURLFactory()).getServerURL(context).toString() + + ((XWikiServletURLFactory) context.getURLFactory()).getContextPath()); + + if (discovered.isVersion2()) { + // OpenID 2.0 supports HTML form redirection which allows payloads >2048 bytes + vcontext.put("op_endpoint", auth_request.getDestinationUrl(false)); + vcontext.put("openid_parameters", auth_request.getParameterMap()); + template = "openid_form_redirect"; + return false; + } else { + // the only method supported in OpenID 1.x is a HTTP-redirect (GET) to the OpenID Provider endpoint (the + // redirect-URL usually limited ~2048 bytes) + sendRedirect(response, auth_request.getDestinationUrl(true)); + return true; + } + } catch (OpenIDException e) { + context.put("message", "register_openid_discovery_failed"); + } catch (Exception e) { + context.put("message", "register_openid_discovery_failed"); + } + + return false; + } + + /** + * Checks the response of the OpenID provider (OP) and outputs a confirmation form. If the OP returns an error or + * the user cancelled at its site, an error message is shown to the user. + * + * @param context + * @throws XWikiException + */ + protected int confirmAttachingOpenId(XWikiContext context) throws XWikiException + { + XWikiRequest request = context.getRequest(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + // extract the parameters from the authentication response + // (which comes in as a HTTP request from the OpenID provider) + ParameterList openid_response = new ParameterList(request.getParameterMap()); + + // retrieve the previously stored discovery information + DiscoveryInformation discovered = (DiscoveryInformation) request.getSession().getAttribute("openid-discovery"); + + // extract the receiving URL from the HTTP request + StringBuffer receiving_url = request.getRequestURL(); + String query_string = request.getQueryString(); + if (query_string != null && query_string.length() > 0) + receiving_url.append("?").append(request.getQueryString()); + + // verify the response; ConsumerManager needs to be the same + // (static) instance used to place the authentication request + VerificationResult verification = manager.verify(receiving_url.toString(), openid_response, discovered); + + // examine the verification result and extract the verified identifier + Identifier verified = verification.getVerifiedId(); + + if (verified != null) { + // check if this OpenID is already registered + if (OpenIdHelper.findUser(verified.getIdentifier(), context) != null) { + vcontext.put("openid_identifier", verified.getIdentifier()); + return -3; + } + + // OpenID not used yet, continue with registration (ask the user for confirmation) + vcontext.put("openid_identifier", verified.getIdentifier()); + return 1; + } else { + // authentication failed, show and log error message + if (openid_response.getParameter("openid.mode") != null + && openid_response.getParameter("openid.mode").getValue().equals("cancel")) { + return -5; // OpenID discovery cancelled + } else { + if (log.isInfoEnabled() && openid_response.getParameter("error") != null) { + log.info("OpenID login failed (error: " + + openid_response.getParameter("openid.error").getValue() + ")"); + } + return -6; // OpenID discovery failed + } + } + } catch (OpenIDException e) { + return -6; // OpenID discovery failed + } + } + + /** + * Attaches an OpenID to an existing user. The method checks if the password is correct and if so the OpenID is + * attached to the user. + * + * @param context + * @return an status code describing the success or failure of the process + * @throws XWikiException + */ + protected int attachOpenID(XWikiContext context) throws XWikiException + { + XWikiRequest request = context.getRequest(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + String openid_identifier = request.getParameter("openid_identifier"); + String password = request.getParameter("password"); + + if (openid_identifier == null || openid_identifier.length() == 0) { + return -2; // No OpenID passed + } + + if (password == null || password.length() == 0) { + vcontext.put("openid_identifier", openid_identifier); + return -4; // Empty passwords are not allowed + } + + // Get the user document + XWikiDocument user_doc; + user_doc = context.getWiki().getDocument(context.getXWikiUser().getUser(), context); + + // Check if the user has already attached an OpenID + if (user_doc.getObject("XWiki.OpenIdIdentifier") != null) { + return -3; // Error! User needs to delete his OpenID first + } + + // Check if the passed password is valid + if (user_doc.getObject("XWiki.XWikiUsers") != null) { + String passwd = user_doc.getStringValue("XWiki.XWikiUsers", "password"); + password = + ((PasswordClass) context.getWiki().getClass("XWiki.XWikiUsers", context).getField("password")) + .getEquivalentPassword(passwd, password); + + if (password.equals(passwd) == false) { + vcontext.put("openid_identifier", openid_identifier); + return -4; // Wrong password + } + } + + // Attach the OpenID + if (OpenIdHelper.attachOpenIdToUser(user_doc, openid_identifier, context) == false) { + return -5; // Internal error + } + + // Successfully attached the OpenID to the user account + vcontext.put("username", context.getWiki().getUserName(context.getXWikiUser().getUser(), context)); + vcontext.put("openid_identifier", openid_identifier); + return 2; + } +} Index: xwiki-core/src/main/java/com/xpn/xwiki/web/RegisterAction.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/web/RegisterAction.java (revision 11817) +++ xwiki-core/src/main/java/com/xpn/xwiki/web/RegisterAction.java (working copy) @@ -20,41 +20,284 @@ */ package com.xpn.xwiki.web; +import java.util.List; + import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; -import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.plugin.openid.OpenIdHelper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.velocity.VelocityContext; -public class RegisterAction extends XWikiAction { - public boolean action(XWikiContext context) throws XWikiException { +import org.openid4java.consumer.ConsumerManager; +import org.openid4java.consumer.VerificationResult; +import org.openid4java.discovery.Identifier; +import org.openid4java.discovery.DiscoveryInformation; +import org.openid4java.message.ax.AxMessage; +import org.openid4java.message.ax.FetchRequest; +import org.openid4java.message.ax.FetchResponse; +import org.openid4java.message.sreg.SRegMessage; +import org.openid4java.message.sreg.SRegRequest; +import org.openid4java.message.sreg.SRegResponse; +import org.openid4java.message.*; +import org.openid4java.OpenIDException; + +/** + * Action used to register new users. This action implements both, registration for classical user accounts with user + * name and password and users using OpenID. + */ +public class RegisterAction extends XWikiAction +{ + private static final Log log = LogFactory.getLog(RegisterAction.class); + + private String template = "register"; + + public boolean action(XWikiContext context) throws XWikiException + { XWiki xwiki = context.getWiki(); XWikiRequest request = context.getRequest(); XWikiResponse response = context.getResponse(); - XWikiDocument doc = context.getDoc(); + template = "register"; + String register = request.getParameter("register"); - if ((register!=null)&&(register.equals("1"))) { + if ((register != null) && (register.equals("1"))) { int useemail = xwiki.getXWikiPreferenceAsInt("use_email_verification", 0, context); int result; - if (useemail==1) - result = xwiki.createUser(true, "edit", context); + if (useemail == 1) + result = xwiki.createUser(true, "edit", context); else - result = xwiki.createUser(context); + result = xwiki.createUser(context); VelocityContext vcontext = (VelocityContext) context.get("vcontext"); vcontext.put("reg", new Integer(result)); + } else if ((register != null) && (register.equals("openid-discover"))) { + if (discoverOpenId(context)) + return false; + } else if ((register != null) && (register.equals("openid-confirm"))) { + String confirm = request.getParameter("register-confirm"); + if ("1".equals(confirm) == false) { + confirmOpenIdRegistration(context); + return true; + } else { + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + vcontext.put("reg", new Integer(registerOpenId(context))); + } } String redirect = Utils.getRedirect(request, null); - if (redirect==null) + if (redirect == null) return true; else { sendRedirect(response, redirect); return false; } - } - - public String render(XWikiContext context) throws XWikiException { - return "register"; - } + } + + public String render(XWikiContext context) throws XWikiException + { + return template; + } + + /** + * Starts registration of an OpenID user. The OpenID provider belonging to the entered OpenID identifier is searched + * and the user is redirected to it to authenticate there. This processed is used to assure that the entered OpenID + * is valid and in possession of that user. If discovery fails, an error message is shown. + * + * @param context + * @return returns true if a redirect was sent, otherwise false. + * @author Markus Lanthaler + */ + protected boolean discoverOpenId(XWikiContext context) + { + XWikiRequest request = context.getRequest(); + XWikiResponse response = context.getResponse(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + + // Check for empty OpenID identifier + String openid_identifier = request.getParameter("openid_identifier"); + if (openid_identifier == null || openid_identifier.equals("")) { + vcontext.put("reg", new Integer(-14)); + return false; + } + + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + String return_to_url = + context.getWiki().getExternalURL("Register", "register", "register=openid-confirm", context); + + // perform discovery on the user-supplied identifier + List discoveries = manager.discover(openid_identifier); + + // attempt to associate with the OpenID provider and retrieve one service endpoint for authentication + DiscoveryInformation discovered = manager.associate(discoveries); + request.getSession().setAttribute("openid-discovery", discovered); + + // obtain a AuthRequest message to be sent to the OpenID provider + AuthRequest auth_request = manager.authenticate(discovered, return_to_url); + + // set the realm + auth_request.setRealm(((XWikiServletURLFactory) context.getURLFactory()).getServerURL(context).toString() + + ((XWikiServletURLFactory) context.getURLFactory()).getContextPath()); + + // attribute exchange (request user data from the OP to speed-up the registration process) + FetchRequest att_exchange = FetchRequest.createFetchRequest(); + att_exchange.addAttribute("email", "http://schema.openid.net/contact/email", true); + att_exchange.addAttribute("firstname", "http://axschema.org/namePerson/first", true); + att_exchange.addAttribute("lastname", "http://axschema.org/namePerson/last", true); + + SRegRequest simple_reg_req = SRegRequest.createFetchRequest(); + simple_reg_req.addAttribute("fullname", true); + simple_reg_req.addAttribute("firstname", true); + simple_reg_req.addAttribute("lastname", true); + simple_reg_req.addAttribute("nickname", true); + simple_reg_req.addAttribute("email", true); + + auth_request.addExtension(att_exchange); + auth_request.addExtension(simple_reg_req); + + if (discovered.isVersion2()) { + // OpenID 2.0 supports HTML form redirection which allows payloads >2048 bytes + vcontext.put("op_endpoint", auth_request.getDestinationUrl(false)); + vcontext.put("openid_parameters", auth_request.getParameterMap()); + template = "openid_form_redirect"; + return false; + } else { + // the only method supported in OpenID 1.x is a HTTP-redirect (GET) to the OpenID Provider endpoint (the + // redirect-URL usually limited ~2048 bytes) + sendRedirect(response, auth_request.getDestinationUrl(true)); + return true; + } + } catch (OpenIDException e) { + context.put("message", "register_openid_discovery_failed"); + } catch (Exception e) { + context.put("message", "register_openid_discovery_failed"); + } + + return false; + } + + /** + * Checks the response of the OpenID provider (OP) and outputs a confirmation form. If the OP returns an error or + * the user cancelled at its site, an error message is shown to the user. + * + * @param context + * @throws XWikiException + * @author Markus Lanthaler + */ + protected void confirmOpenIdRegistration(XWikiContext context) throws XWikiException + { + XWikiRequest request = context.getRequest(); + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + + try { + ConsumerManager manager = OpenIdHelper.getConsumerManager(); + + // extract the parameters from the authentication response + // (which comes in as a HTTP request from the OpenID provider) + ParameterList openid_response = new ParameterList(request.getParameterMap()); + + // retrieve the previously stored discovery information + DiscoveryInformation discovered = (DiscoveryInformation) request.getSession().getAttribute("openid-discovery"); + + // extract the receiving URL from the HTTP request + StringBuffer receiving_url = request.getRequestURL(); + String query_string = request.getQueryString(); + if (query_string != null && query_string.length() > 0) + receiving_url.append("?").append(request.getQueryString()); + + // verify the response; ConsumerManager needs to be the same + // (static) instance used to place the authentication request + VerificationResult verification = manager.verify(receiving_url.toString(), openid_response, discovered); + + // examine the verification result and extract the verified identifier + Identifier verified = verification.getVerifiedId(); + + if (verified != null) { + // check if this OpenID is already registered + if (OpenIdHelper.findUser(verified.getIdentifier(), context) != null) { + vcontext.put("reg", new Integer(-13)); + vcontext.put("openid_identifier", verified.getIdentifier()); + return; + } + + // OpenID not used yet, continue with registration (ask the user for confirmation) + vcontext.put("reg", new Integer(2)); + vcontext.put("openid_identifier", verified.getIdentifier()); + + AuthSuccess auth_success = (AuthSuccess) verification.getAuthResponse(); + + String email = null; + String firstname = null; + String lastname = null; + + if (auth_success.hasExtension(AxMessage.OPENID_NS_AX)) { + FetchResponse fetch_resp = (FetchResponse) auth_success.getExtension(AxMessage.OPENID_NS_AX); + email = (String) fetch_resp.getAttributeValues("email").get(0); + firstname = (String) fetch_resp.getAttributeValues("firstname").get(0); + lastname = (String) fetch_resp.getAttributeValues("lastname").get(0); + } + + if (auth_success.getExtension(SRegMessage.OPENID_NS_SREG) instanceof SRegResponse) { + SRegResponse simple_reg_resp = (SRegResponse) auth_success.getExtension(SRegMessage.OPENID_NS_SREG); + + if (email == null) + email = simple_reg_resp.getAttributeValue("email"); + + if (firstname == null && lastname == null) + firstname = simple_reg_resp.getAttributeValue("fullname"); + } + + vcontext.put("email", email); + vcontext.put("first_name", firstname); + vcontext.put("last_name", lastname); + } else { + // authentication failed, show and log error message + if (openid_response.getParameter("openid.mode") != null + && openid_response.getParameter("openid.mode").getValue().equals("cancel")) { + context.put("message", "register_openid_discovery_cancelled"); + } else { + if (log.isInfoEnabled() && openid_response.getParameter("error") != null) { + log.info("OpenID login failed (error: " + + openid_response.getParameter("openid.error").getValue() + ")"); + } + context.put("message", "register_openid_discovery_failed"); + } + } + } catch (OpenIDException e) { + context.put("message", "register_openid_discovery_failed"); + } + } + + /** + * Completes the registration of an OpenID user. The user name is created automatically based on the OpenID + * identifier by {@link OpenIdHelper#openIdIdentifierToUsername}. + * + * @param context + * @return an status code describing the success or failure of the registration + * @throws XWikiException + * @author Markus Lanthaler + * @see OpenIdHelper#openIdIdentifierToUsername + */ + protected int registerOpenId(XWikiContext context) throws XWikiException + { + XWikiRequest request = context.getRequest(); + String openid_identifier = request.getParameter("openid_identifier"); + String firstname = request.getParameter("register_first_name"); + String lastname = request.getParameter("register_last_name"); + String email = request.getParameter("register_email"); + + int result = OpenIdHelper.createUser(openid_identifier, firstname, lastname, email, context); + if (result == 3) { + // user registration successful + VelocityContext vcontext = (VelocityContext) context.get("vcontext"); + vcontext.put("username", context.getWiki().getUserName(OpenIdHelper.findUser(openid_identifier, context), + context)); + vcontext.put("openid_identifier", openid_identifier); + } + + return result; + } } Index: xwiki-core/src/main/java/com/xpn/xwiki/web/XWikiServletURLFactory.java =================================================================== --- xwiki-core/src/main/java/com/xpn/xwiki/web/XWikiServletURLFactory.java (revision 11817) +++ xwiki-core/src/main/java/com/xpn/xwiki/web/XWikiServletURLFactory.java (working copy) @@ -134,12 +134,12 @@ return servletPath; } - private URL getServerURL(XWikiContext context) throws MalformedURLException + public URL getServerURL(XWikiContext context) throws MalformedURLException { return getServerURL(context.getDatabase(), context); } - private URL getServerURL(String xwikidb, XWikiContext context) throws MalformedURLException + public URL getServerURL(String xwikidb, XWikiContext context) throws MalformedURLException { URL serverURL = this.serverURL; if (context.getRequest() != null) { // necessary to the tests Index: xwiki-core/src/main/resources/ApplicationResources.properties =================================================================== --- xwiki-core/src/main/resources/ApplicationResources.properties (revision 11817) +++ xwiki-core/src/main/resources/ApplicationResources.properties (working copy) @@ -527,12 +527,48 @@ admin.adminappnotinstalled=The administration application is not installed. Since XWiki Enterprise 1.5 the Administration is distributed as an application. You can download it from {0}. #login +login_username_password_desc=with your username and password +login_openid_desc=... or with your OpenID nousername=No user name given +noopenid=Please enter your OpenID identifier to log in nopassword=No password given wronguser=Wrong user name wrongpassword=Wrong password loginfailed=Internal error +openidlogin_cancelled=The login process was cancelled at the OpenID provider site +openid_not_associated=No user was found for this OpenID +#openid +openid.common.discovery_cancelled=The login process was cancelled at the OpenID provider site +openid.common.discovery_failed=The discovery of the OpenID failed +openid.common.identifier_field_label=OpenID + +#openid form redirect +openid.form_redirect.title=OpenID login in progress +openid.form_redirect.description=You are redirected to your OpenID provider in a few seconds. Please wait... +openid.form_redirect.button=Continue... + +#openid registration form +openid.registration.welcome=If you have an OpenID or i-name you can speed-up your registration by entering it here +openid.registration.discovery_submit=Register with OpenID +openid.registration.confirmation=Verify the form below and complete your OpenID registration +openid.registration.confirmation_submit=Create OpenID account +openid.registration.invalidOpenID=Enter a valid OpenID to continue with registration +openid.registration.openIdAlreadyRegistered=The OpenID "{0}" is already registered. You can directly log in if it is in your possession. +openid.registration.successful=Successfully registered user {0} ({1}). + +#attach openid to existing user +openid.attach.title=Associate an OpenID with your user account +openid.attach.welcome=Enter your OpenID to attach it to your user account. +openid.attach.confirm=Enter your password to attach the OpenID shown below to your user account. +openid.attach.submit=Attach OpenID +openid.attach.alreadyAssociatedOpenId=You have already associated an OpenID with your user account. +openid.attach.openIdAlreadyRegistered=The OpenID "{0}" is already registered with another user account. You can directly log in if it is in your possession. +openid.attach.passwordInvalid=The entered password is invalid +openid.attach.internalError=An internal error occurred and the OpenID could not be associated with you user account. Please contact the administrator. +openid.attach.successful=Successfully attached the OpenID "{0}" to {0}. + + switchto=Switch to sectionEdit=Sectional Editing @@ -619,6 +655,8 @@ registerfailed=Registration has failed registerfailedcode=code registersuccessful=Registration successful +register_openid_discovery_cancelled=The registration with OpenID was cancelled at the OpenID provider site +register_openid_discovery_failed=The registration with OpenID failed due to an internal error leftPanels=Left Panels rightPanels=Right Panels @@ -764,6 +802,7 @@ core.comment.updateClassPropertyName=Updated class property name core.comment.createdUser=Created user core.comment.addedUserToGroup=Added user to group +core.comment.addedOpenIdIdentifier=Added OpenID identifier core.comment.rollback=Rollback to version {0} core.comment.updateContent=Update Content core.comment.uploadAttachmentComment=Upload new attachment {0} Index: xwiki-core/src/main/resources/JcrQueries.properties =================================================================== --- xwiki-core/src/main/resources/JcrQueries.properties (revision 11817) +++ xwiki-core/src/main/resources/JcrQueries.properties (working copy) @@ -3,3 +3,4 @@ getAllDocuments=//element(*, xwiki:document)/@fullName listGroupsForUser=//element(*, xwiki:document)[obj/XWiki/XWikiGroups[@xp:member=:{username} or @xp:member=:{shortname} or @xp:member=:{veryshortname}]/@fullName getAllUsers=//element(*, xwiki:document)[obj/XWiki/XWikiUsers]/@fullName +getUserDocByOpenIdIdentifier=//element(*, xwiki:document)[obj/XWiki/OpenIdIdentifier/@xp:identifier=:{identifier}]/@fullName Index: xwiki-core/src/main/resources/queries.hbm.xml =================================================================== --- xwiki-core/src/main/resources/queries.hbm.xml (revision 11817) +++ xwiki-core/src/main/resources/queries.hbm.xml (working copy) @@ -23,4 +23,7 @@ select doc.fullName from XWikiDocument as doc, BaseObject as obj where obj.name=doc.fullName and obj.className='XWiki.XWikiUsers' + + select doc.fullName from XWikiDocument as doc, BaseObject as obj, StringProperty as prop where doc.fullName=obj.name and obj.className='XWiki.OpenIdIdentifier' and obj.id=prop.id.id and prop.id.name='identifier' and prop.value=:identifier +