/* * 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.security.GeneralSecurityException; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.lang3.StringUtils; import org.brickred.socialauth.AuthProvider; import org.brickred.socialauth.Permission; import org.brickred.socialauth.Profile; import org.brickred.socialauth.SocialAuthConfig; import org.brickred.socialauth.SocialAuthManager; import org.brickred.socialauth.util.SocialAuthUtil; import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.context.Execution; import org.xwiki.environment.Environment; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReferenceValueProvider; import org.xwiki.query.Query; import org.xwiki.query.QueryException; import org.xwiki.query.QueryManager; import org.xwiki.social.authentication.ProfilePictureProviderTransformer; 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.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; @Component public class DefaultSocialAuthManager implements SocialAuthenticationManager, SocialAuthConstants { private static final String DEFAULT_PROFILE_PICTURE_FILENAME = "profile.jpg"; private static final String GLOBAL_CONFIGURATION_KEY = "xwiki.authentication.socialLogin.globalConfiguration"; private static final String EXTRA_REGISTRATION_STEP_DOCUMENT = "XWiki.SocialLoginRegister"; @Inject private Logger logger; @Inject private Environment environment; @Inject private Execution execution; @Inject private QueryManager queryManager; @Inject private SocialAuthConfiguration configuration; @Inject private PasswordCryptoService passwordCryptoService; @Inject private Map<String, ProfilePictureProviderTransformer> profilePictureTransformers; @Inject private EntityReferenceValueProvider valueProvider; private SocialAuthConfig config; @Override public void associateAccount(String providerId) throws SocialAuthException { XWikiContext context = getContext(); HttpServletRequest request = context.getRequest(); AuthProvider provider; try { if (StringUtils.isBlank(request.getParameter(CALLBACK_PARAMETER))) { String url = request.getRequestURL() + "?" + request.getQueryString() + "&" + CALLBACK_PARAMETER + "=1&" + PROVIDER_PARAMETER + "=" + providerId; this.requestConnection(providerId, url); } else { SocialAuthSession session = (SocialAuthSession) request.getSession().getAttribute(SOCIAL_AUTH_SESSION_ATTRIBUTE); provider = session.getAuthManager().connect(SocialAuthUtil.getRequestParametersMap(request)); session.putAuthProvider(providerId, provider); Profile profile = provider.getUserProfile(); if (getUser(providerId, profile.getValidatedId()) != null) { throw new SocialAuthException( "Refusing to associate account as it is already associated with a user on this wiki."); } this.addSocialProfileToUser(profile, getContext().getUserReference()); } } catch (Exception e) { throw new SocialAuthException("Failed to associate account", e); } } @Override public void ensureConnected(String providerId) throws SocialAuthException { XWikiContext context = getContext(); HttpServletRequest request = context.getRequest(); AuthProvider provider; try { if (StringUtils.isBlank(request.getParameter(CALLBACK_PARAMETER))) { String url = request.getRequestURL() + "?" + StringUtils.defaultIfBlank(request.getQueryString(), "") + "&" + CALLBACK_PARAMETER + "=1&" + PROVIDER_PARAMETER + "=" + providerId; this.requestConnection(providerId, url); } else { SocialAuthSession session = (SocialAuthSession) request.getSession().getAttribute(SOCIAL_AUTH_SESSION_ATTRIBUTE); provider = session.getAuthManager().connect(SocialAuthUtil.getRequestParametersMap(request)); session.putAuthProvider(providerId, provider); } } catch (Exception e) { throw new SocialAuthException("Failed to associate account", e); } } @Override public DocumentReference connect(Map<String, String> requestParameters) throws SocialAuthException { AuthProvider provider; SocialAuthSession session = getSession(); try { SocialAuthManager manager = session.getAuthManager(); provider = manager.connect(requestParameters); Profile profile = provider.getUserProfile(); // check eventual domain restriction String domainRestriction = configuration.getDomainRestriction(); if (!StringUtils.isBlank(domainRestriction)) { if (StringUtils.isBlank(profile.getEmail()) || !profile.getEmail().endsWith(domainRestriction)) { // user email does not match the proper domain, we need to refuse it XWikiContext context = getContext(); context.put("message", "xwiki.socialLogin.unauthorizedDomainError"); throw new SocialAuthException( "Failed to validate connection because email is not matching the authorized domain"); } } boolean isGlobalConfiguration = isGlobalConfiguration(); XWikiContext context = getContext(); String currentDatabase = context.getDatabase(); if (isGlobalConfiguration) { // we need to make sure this happens in the main wiki if the configuration says so context.setDatabase(getMainWikiName()); } try { DocumentReference user = getUser(profile.getProviderId(), profile.getValidatedId()); // FIXME use a random in a singleton instead, as somebody could use the persistent cookie to forge the // encrypted password session.putAuthProvider(profile.getProviderId(), provider); session.setCurrentProvider(profile.getProviderId()); if (user == null) { if (configuration.isAutomaticUserCreation()) { user = this.createUser(profile, this.computeUsername(profile)); } else { getResponse().sendRedirect( context.getWiki().getURL(EXTRA_REGISTRATION_STEP_DOCUMENT, "view", context)); return null; } } XWikiDocument userDocument = getContext().getWiki().getDocument(user, getContext()); BaseObject object = userDocument.getXObject(SOCIAL_LOGIN_PROFILE_CLASS, "provider", profile.getProviderId()); String password = object.getStringValue("password"); this.setPassword(password); return user; } finally { if (isGlobalConfiguration) { context.setDatabase(currentDatabase); } } } catch (Exception e) { throw new SocialAuthException("Failed to validate connection", e); } } @Override public DocumentReference createUser(Map<String, String> extraProperties) throws XWikiException, SocialAuthException { SocialAuthSession session = getSession(); if (session == null || session.getProfile() == null) { throw new SocialAuthException("Illegal attempt at creating a user that is not associated"); } return this.createUser(this.computeUsername(session.getProfile()), extraProperties); } @Override public DocumentReference createUser(String username, Map<String, String> extraProperties) throws XWikiException, SocialAuthException { SocialAuthSession session = getSession(); if (session == null || session.getProfile() == null) { throw new SocialAuthException("Illegal attempt at creating a user that is not associated"); } return this.createUser(session.getProfile(), username, extraProperties); } @Override public SocialAuthSession getSession() { HttpSession httpSession = getRequest().getSession(); SocialAuthSession session = (SocialAuthSession) httpSession.getAttribute(SOCIAL_AUTH_SESSION_ATTRIBUTE); return session; } @Override public DocumentReference getUser(String provider, String id) { // we need to make sure this happens in the main wiki if the configuration says so // this is already done in connect() but getUser can also be called from the Authenticator boolean isGlobalConfiguration = isGlobalConfiguration(); XWikiContext context = getContext(); String currentDatabase = context.getDatabase(); if (isGlobalConfiguration) { context.setDatabase(getMainWikiName()); } try { String queryStatement = "from doc.object(XWiki.XWikiUsers) as user, doc.object(XWiki.SocialLoginProfileClass)" + " as profile where profile.provider = :provider and profile.validatedId = :validated"; Query query = this.queryManager.createQuery(queryStatement, Query.XWQL); query.bindValue("provider", provider); query.bindValue("validated", id); List<String> results = query.execute(); for (String reference : results) { return getContext().getWiki().getDocument(reference, getContext()).getDocumentReference(); } return null; } catch (QueryException e) { this.logger.error("Failed to query for user with provider [{}] and id [{}]", provider, id); return null; } catch (XWikiException e) { this.logger.error("Failed to query for user with provider [{}] and id [{}]", provider, id); return null; } finally { if (isGlobalConfiguration) { context.setDatabase(currentDatabase); } } } @Override public boolean hasProvider(DocumentReference user, String provider) { XWikiDocument userDocument; try { userDocument = getContext().getWiki().getDocument(user, getContext()); BaseObject object = userDocument.getXObject(SOCIAL_LOGIN_PROFILE_CLASS, "provider", provider); return object != null; } catch (XWikiException e) { this.logger .error(MessageFormat.format("Failed to determine if user [{0}] has associated provider [{1}]", user, provider), e); return false; } } @Override public boolean isConnected() { return isConnected(getSession()); } @Override public boolean isConnected(String provider) { return isConnected(getSession(), provider); } @Override public void requestConnection(String provider, String returnUrl) throws SocialAuthException { HttpSession httpSession = getRequest().getSession(); HttpServletResponse response = getResponse(); try { SocialAuthManager manager = new SocialAuthManager(); manager.setSocialAuthConfig(this.getSocialAuthConfig()); // Save the manager in session : we will need this one on the way back from OAuth to validate the response SocialAuthSession session = new SocialAuthSession(manager); session.setCurrentProvider(provider); httpSession.setAttribute(SOCIAL_AUTH_SESSION_ATTRIBUTE, session); Permission urlPermission = null; // Check for custom permissions in the OAuth config file String customPermissions = null; for (Object entry : this.getSocialAuthConfig().getApplicationProperties().keySet()) { String cKey = (String) entry; if (cKey.contains(provider) && cKey.contains("custom_permission")) { customPermissions = (String) this.getSocialAuthConfig().getApplicationProperties().get(cKey); break; } } if(!StringUtils.isBlank(customPermissions)) { logger.debug("Using custom permissions for scope:" + customPermissions); manager.getSocialAuthConfig().getProviderConfig(provider).setCustomPermissions(customPermissions); urlPermission = Permission.CUSTOM; } String url = manager.getAuthenticationUrl(provider, returnUrl, urlPermission); logger.debug("Redirecting to OAuth endpoint URL : " + url); try { response.sendRedirect(url); } catch (java.lang.IllegalStateException e) { // Avoid "response already committed" exception in the logs... // FIXME need to find a way to perform a redirection cleanly from an authenticator. } } catch (Exception e) { throw new SocialAuthException("Error when requesting connection", e); } } @Override public boolean userExists(String provider, String id) { return getUser(provider, id) != null; } // ///////////////////////////////////////////////////////////////////////////////////////////// private void addSocialProfileToUser(Profile profile, DocumentReference user) throws SocialAuthException { boolean isGlobalConfiguration = isGlobalConfiguration(); XWikiContext context = getContext(); String currentDatabase = context.getDatabase(); if (isGlobalConfiguration) { // we need to make sure this happens in the main wiki if the configuration says so // this is already done in connect() but getUser can also be called from the Authenticator context.setDatabase(getMainWikiName()); } try { XWikiDocument userDoc = context.getWiki().getDocument(user, context); String generatedPassword = getContext().getWiki().generateRandomString(16); BaseObject socialProfile = userDoc.getXObject(SOCIAL_LOGIN_PROFILE_CLASS, true, context); BaseObject userObject = userDoc.getXObject(XWIKI_USER_CLASS_REF, false, context); if (userObject == null) { throw new SocialAuthException("Cannot associate a social profile to a non-user page"); } if (StringUtils.isBlank(userObject.getStringValue("first_name"))) { userObject.set("first_name", profile.getFirstName(), context); } if (StringUtils.isBlank(userObject.getStringValue("last_name"))) { userObject.set("last_name", profile.getLastName(), context); } if (StringUtils.isBlank(userObject.getStringValue("email"))) { userObject.set("email", profile.getEmail(), context); } if (!StringUtils.isBlank(profile.getProfileImageURL()) && StringUtils.isBlank(userObject.getStringValue("avatar"))) { String profilePictureURL = profile.getProfileImageURL(); if (this.profilePictureTransformers.containsKey(profile.getProviderId())) { // Transform profile picture URL if necessary for this provider profilePictureURL = this.profilePictureTransformers.get(profile.getProviderId()).transform(profilePictureURL); } GetMethod get = new GetMethod(profilePictureURL); try { XWikiAttachment attachment = new XWikiAttachment(userDoc, DEFAULT_PROFILE_PICTURE_FILENAME); userDoc.getAttachmentList().add(attachment); HttpClient httpClient = new HttpClient(); httpClient.getParams().setBooleanParameter("http.connection.stalecheck", true); int httpStatus = httpClient.executeMethod(get); if (httpStatus == HttpStatus.SC_OK) { attachment.setContent(get.getResponseBodyAsStream()); attachment.setAuthor(userDoc.getAuthor()); attachment.setDoc(userDoc); userObject.set("avatar", DEFAULT_PROFILE_PICTURE_FILENAME, context); } else { this.logger.debug("Failed to load image: status is " + httpStatus); } } catch (Exception e) { this.logger.warn("Error attaching social profile picture to profile", e); // Nevermind, ain't gonna have a profile picture. } finally { get.releaseConnection(); } } socialProfile.set("provider", profile.getProviderId(), context); socialProfile.set("fullName", profile.getFullName(), context); socialProfile.set("firstName", profile.getFirstName(), context); socialProfile.set("lastName", profile.getLastName(), context); socialProfile.set("displayName", profile.getDisplayName(), context); socialProfile.set("email", profile.getEmail(), context); socialProfile.set("profileImageURL", profile.getProfileImageURL(), context); socialProfile.set("gender", profile.getGender(), context); if (profile.getDob() != null) { socialProfile.set("dob", profile.getDob().toString(), context); } socialProfile.set("validatedId", profile.getValidatedId(), context); socialProfile.set("country", profile.getCountry(), context); socialProfile.set("location", profile.getLocation(), context); socialProfile.set("password", generatedPassword, context); this.setPassword(generatedPassword); context.getWiki().saveDocument(userDoc, context.getMessageTool().get("xwiki.socialLogin.updatedSocialProfile"), true, context); } catch (XWikiException e) { this.logger.error("Failed to merge or create user", e); } finally { if (isGlobalConfiguration) { context.setDatabase(currentDatabase); } } } private String computeUsername(Profile profile) { // TODO let the format be defined in configuration String username = profile.getDisplayName(); if (StringUtils.isBlank(username)) { username = profile.getFirstName() + profile.getLastName(); } if (StringUtils.isBlank(username)) { username = profile.getProviderId() + "-" + profile.getValidatedId(); } return getContext().getWiki().getUniquePageName("XWiki", username, getContext()); } private DocumentReference createUser(Profile profile, String username) throws XWikiException, SocialAuthException { return this.createUser(profile, username, Collections.<String, String> emptyMap()); } private DocumentReference createUser(Profile profile, String username, Map<String, String> extraProperties) throws XWikiException, SocialAuthException { String userDocumentName = "XWiki." + username; if (isGlobalConfiguration()) { // we need to make sure we create the user globally if the configuration says so userDocumentName = getMainWikiName() + ":" + userDocumentName; } XWikiContext context = getContext(); Map<String, String> properties = new HashMap<String, String>(extraProperties); properties.put("active", "1"); properties.put("email", profile.getEmail()); properties.put("first_name", profile.getFirstName()); properties.put("last_name", profile.getLastName()); // We don't put the same password as the one of the social profile properties.put("password", getContext().getWiki().generateRandomString(16)); context.getWiki().createUser(username, properties, context); XWikiDocument userDoc = context.getWiki().getDocument(userDocumentName, context); this.addSocialProfileToUser(profile, userDoc.getDocumentReference()); return userDoc.getDocumentReference(); } private XWikiContext getContext() { return (XWikiContext) this.execution.getContext().getProperty("xwikicontext"); } private String getEncryptionKey() { String key = getContext().getWiki().Param("xwiki.authentication.encryptionKey"); return key; } private String getMainWikiName() { return valueProvider.getDefaultValue(EntityType.WIKI); } private HttpServletRequest getRequest() { return getContext().getRequest(); } private HttpServletResponse getResponse() { return getContext().getResponse(); } private boolean isGlobalConfiguration() { return "1".equals(getContext().getWiki().Param(GLOBAL_CONFIGURATION_KEY)); } private SocialAuthConfig getSocialAuthConfig() { if (this.config == null) { try { Properties properties = new Properties(); properties.load(this.environment.getResourceAsStream("/WEB-INF/oauth_consumer.properties")); config = SocialAuthConfig.getDefault(); config.load(properties); } catch (Exception e) { logger.error("Failed to initialize Social Auth", e); } } return this.config; } private boolean isConnected(SocialAuthSession profile) { return profile != null && this.isConnected(profile, profile.getCurrentProvider()); } private boolean isConnected(SocialAuthSession profile, String provider) { return profile != null && profile.getProfile(provider) != null; } private void setPassword(String password) { try { getSession().setEncryptedPassword(this.passwordCryptoService.encryptText(password, getEncryptionKey())); } catch (GeneralSecurityException e) { // Nothing } } }