/* * 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 org.xwiki.social.authentication.internal; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.GeneralSecurityException; import java.security.Principal; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.brickred.socialauth.util.SocialAuthUtil; import org.securityfilter.realm.SimplePrincipal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.model.reference.DocumentReference; import org.xwiki.social.authentication.SocialAuthConfiguration; import org.xwiki.social.authentication.SocialAuthConstants; import org.xwiki.social.authentication.SocialAuthException; import org.xwiki.social.authentication.SocialAuthSession; import org.xwiki.social.authentication.SocialAuthenticationManager; import org.xwiki.social.legacy.crypto.passwd.PasswordCryptoService; 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.user.impl.xwiki.XWikiAuthServiceImpl; import com.xpn.xwiki.web.Utils; /** * <p> * An authenticator for social networks/OAuth end-points, based on Redbrick SocialAuth. The authenticator allows * optional fall-back on XWiki authentication. * </p> * <p> * When authenticating, this authenticator ensures a successful OAuth handshake has been established, and that a XWiki * user matches the third-party user used against the OAuth endpoint. If no such user exists, then according to the * module's configuration, it is either created automatically, either with optional confirmation steps (such as the * confirmation of the username to use, the request of extra registration information, etc.). * </p> * <p> * Internally the authenticator works the following way : * <ul> * <li>OAuth authentication status is held in memory, in the user session (and is protected with an encrypted password * to prevent status forgery, this will be explained later).</li> * <li>When no such status exists and no OAuth handshake has been requested, this authenticator is not concerned. It * then either leave it to the XWiki authentication service to try and authenticate the request, or leave it * unauthenticated, according to configuration.</li> * <li>Authentication against a particular OAuth end-point (a.k.a a "provider") is considered "requested" when the * request contains a value for the parameter {@link SocialAuthConstants#PROVIDER_PARAMETER}. When such request is * expressed, and assuming the provider is configured properly, the authenticator redirects the user to the third party * site for authorization, providing a callback URL (or "return url"). If the authorization on the third party website * has been granted previously, then the third party web-site will redirect to the return URL directly, if not user * confirmation will be necessary to grant XWiki the authorization to act as a OAuth consumer. When returning from OAuth * (detected by the authenticator by the presence of the {@link SocialAuthConstants#CALLBACK_PARAMETER}, the * authenticator will verify the third party response. If it is valid, it means the OAuth handshake is successful.</li> * <li>A successful handshake is followed by the lookup of a XWiki user that has in its profile page information * matching the provider and the returned third party user ("profile id"). This information is looked up in an object of * class <tt>XWiki.SocialLoginProfileClasss</tt>. If such user exists, it means it's a returning user. The authenticator * then looks up a "password" string held in that <tt>XWiki.SocialLoginProfileClasss</tt> (which is a random string, * unknown to the user), encrypt it with a secret key, and stores it in the user session together in a * {@link SocialAuthSession} object. The authenticator then triggers another * {@link #authenticate(String, String, XWikiContext)} call, passing as username/password the name of the matched XWiki * document, and the retrieved password. * <li>When authentication is requested and a {@link SocialAuthSession} exists in the user session (potentially meaning * a successful handshake has previously been established), the passed password is compared against the one encrypted in * the session. If they match, the user is considered authenticated, and its "credentials" (which are unknown to the * user) are remembered using the standard XWiki persistent login mechanism (that uses encrypted cookies), so that * following requests continue to be successfully authenticated. If they don't match, no user is authenticated.</li> * <li>When a successful OAuth handshake is followed by a looked that finds no matching XWiki user, then the user can be * either created dynamically, or "manually" with extra confirmation steps, according to the configuration. When the * user is created, the same steps apply as for an returning user.</li> * </p> * <p> * Overall, session forgery is protected against via the fact successful OAuth handshakes are "stamped" with a * clear-text password encrypted with a secret key known only to the internal system. Stealing a user's "password" * (calling ?xpage=xml on its profile page for example) doesn't help to create the fake OAuth status in a session, * without knowing the private key). * </p> * <p> * See {@link SocialAuthConfiguration} for configuration options. * </p> * * @version $Id$ */ public class SocialAuthServiceImpl extends XWikiAuthServiceImpl implements SocialAuthConstants { /** * Logger used for this authenticator. */ private static final Logger LOGGER = LoggerFactory.getLogger(SocialAuthServiceImpl.class); @Override public Principal authenticate(String login, String password, XWikiContext context) throws XWikiException { LOGGER.debug("Social login authenticate..."); HttpServletRequest request = context.getRequest(); SocialAuthenticationManager manager = Utils.getComponent(SocialAuthenticationManager.class); SocialAuthSession session = manager.getSession(); if (StringUtils.isBlank(request.getParameter(PROVIDER_PARAMETER)) && (session == null || session.getCurrentProvider() == null) && !manager.isConnected()) { // Passing along to XWiki authentication // TODO add a parameter in configuration to declare if normal XWiki auth is allowed // or not. LOGGER.debug("No provider given: let XWiki authenticate..."); return super.authenticate(login, password, context); } String provider = StringUtils.defaultIfBlank(session != null ? session.getCurrentProvider() : null, request.getParameter(PROVIDER_PARAMETER)); try { if (!manager.isConnected(provider) || password == null) { // If no social session is present in the user session, we try to associate one for the request // provider by triggering a OAuth handshake. Note this is a two step process, meaning two consecutive // requests will have to go through this call. trySocialAuthConnect(provider, manager, context); return null; } else { // If a social auth session status is actually present in the user session, we validate the passed // password // which comes either from : // * a #checkAuth call triggered in #trySocialConnect // * a #authenticate call triggered by the persistent login manager when authenticating from cookies // // If password matches with the one stored (encrypted) in the session Fthen we validate the connection // returning the user principal. If the password doesn't come from cookies already, then the // authenticator will take care of remembering those credentials in cookies via the persistent login // manager. return validateCredentials(login, password, session, manager, context); } } catch (Exception e) { LOGGER.error("Error while Social login authentication", e); } return null; } /** * Initiates or validates an OAuth handshake. * * @param provider the id of the provider to connect to. Example: "facebook", "twitter", etc. * @param manager the social authentication manager used to connect * @param context the XWiki context * @throws SocialAuthException when something goes wrong at the SocialAuth/OAuth level * @throws XWikiException when something goes wrong at the XWiki level */ private void trySocialAuthConnect(String provider, SocialAuthenticationManager manager, XWikiContext context) throws SocialAuthException, XWikiException { HttpServletRequest request = context.getRequest(); boolean validSession = manager.getSession() != null && manager.getSession().getProfile() != null; if (StringUtils.isBlank(request.getParameter(CALLBACK_PARAMETER)) && !validSession) { // Step 1. // Redirect the request towards the target OAuth endpoint String url = request.getRequestURL() + "?" + CALLBACK_PARAMETER + "=1&" + PROVIDER_PARAMETER + "=" + provider; // FIXME Right now the xredirect parameter is lost because we don't pass the full query string // in the redirect_uri. // There is an issue with some special characters in the redirect_uri that will make Facebook // not validate the request. // See http://stackoverflow.com/questions/4386691/facebook-error-error-validating-verification-code //in case the provider is not Facebook, keep the xredirect parameter String redirect = context.getRequest().getParameter("xredirect"); if (redirect != null && provider != null && !provider.toLowerCase().equals("facebook")) { try { url = url + "&xredirect=" + URLEncoder.encode(redirect, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new SocialAuthException("Bad URL encoding", e); } } manager.requestConnection(provider, url); } else { // Step 2. // Handle response from the OAuth endpoint DocumentReference user; if (manager.getSession() == null || manager.getSession().getProfile() == null) { LOGGER.debug("We back from OAuth URL"); user = manager.connect(SocialAuthUtil.getRequestParametersMap(request)); } else { user = manager.getUser(provider, manager.getSession().getProfile().getValidatedId()); LOGGER.debug("Already a profile in the session. User " + user); } if (user != null) { XWikiDocument document = context.getWiki().getDocument(user, context); BaseObject profileObject = document.getXObject(SOCIAL_LOGIN_PROFILE_CLASS, "provider", provider); String passwd = profileObject.getStringValue("password"); // We have just associated a social profile with the user session, which contains an encrypted password. // Now we need to trigger the actual authentication and persistence of those credentials in cookies to // actually log the user in. The following checkAuth call does just that : // it will trigger a call to #authenticate to verify the (clear-text) password against the one encrypted // in the session, and if they match, it will return the proper principal and remember user credentials // via the persistence login manager so that next requests keep being properly authenticated. this.checkAuth(document.getName(), passwd, "true", context); } } } /** * Validates a username/password against a social profile (coming from memory, and with its password encrypted). * * @param username the username to verify * @param password the password to verify * @param session the social auth session containing the profile to verify against, coming from memory * @param manager the social authentication manager, used here to retrieved a user from the profile * @param context the XWiki context * @return a principal if the test is successful, null otherwise * @throws GeneralSecurityException when something goes wrong decrypting the password in the profile * @throws XWikiException when something goes wrong at the XWiki level */ private Principal validateCredentials(String username, String password, SocialAuthSession session, SocialAuthenticationManager manager, XWikiContext context) throws GeneralSecurityException, XWikiException { LOGGER.debug("Found a social profile in session"); PasswordCryptoService passwordCryptoService = Utils.getComponent(PasswordCryptoService.class); String key = context.getWiki().Param("xwiki.authentication.encryptionKey"); DocumentReference user = manager.getUser(session.getProfile().getProviderId(), session.getProfile().getValidatedId()); XWikiDocument document = context.getWiki().getDocument(user, context); if (!StringUtils.isBlank(session.getEncryptedPassword()) && passwordCryptoService.decryptText(session.getEncryptedPassword(), key).equals(password) && username.equals(document.getName())) { LOGGER.debug("Password match, returning principal " + document.getName()); return new SimplePrincipal(document.getPrefixedFullName()); } LOGGER.debug("Password null or password mismatch"); if (!StringUtils.isBlank(password)) { LOGGER.debug("Password null"); context.getRequest().getSession().removeAttribute(SOCIAL_AUTH_SESSION_ATTRIBUTE); } // Leave to the XWiki authentication // TODO check a "trylocal" parameter simalar to LDAP auth return super.authenticate(username, password, context); } }