package org.craftercms.security.utils.social; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.craftercms.commons.crypto.CryptoException; import org.craftercms.commons.crypto.TextEncryptor; import org.craftercms.profile.api.Profile; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionData; import org.springframework.social.connect.UserProfile; /** * Utility methods related with connections with providers. * * @author avasquez */ public class ConnectionUtils { public static final String CONNECTIONS_ATTRIBUTE_NAME = "connections"; public static final String FIRST_NAME_ATTRIBUTE_NAME = "firstName"; public static final String LAST_NAME_ATTRIBUTE_NAME = "lastName"; public static final String DISPLAY_NAME_ATTRIBUTE_NAME = "displayName"; public static final String AVATAR_LINK_ATTRIBUTE_NAME = "avatarLink"; /** * Creates a new map from the specified {@link ConnectionData}. Used when * connection data needs to be stored in a profile. * * @param connectionData the connection data to convert * @param encryptor the encryptor used to encrypt the accessToken, secret and refreshToken (optional) * * @return the connection data as a map */ public static Map<String, Object> connectionDataToMap(ConnectionData connectionData, TextEncryptor encryptor) throws CryptoException { Map<String, Object> map = new HashMap<>(); map.put("providerUserId", connectionData.getProviderUserId()); map.put("displayName", connectionData.getDisplayName()); map.put("profileUrl", connectionData.getProfileUrl()); map.put("imageUrl", connectionData.getImageUrl()); map.put("accessToken", encrypt(connectionData.getAccessToken(), encryptor)); map.put("secret", encrypt(connectionData.getSecret(), encryptor)); map.put("refreshToken", encrypt(connectionData.getRefreshToken(), encryptor)); map.put("expireTime", connectionData.getExpireTime()); return map; } /** * Creates a new instance of {@link ConnectionData} from the specified map. * Used when connection data needs to be retrieved from a profile. * * @param providerId the provider ID of the connection (which is not stored in the map) * @param map the map to convert * @param encryptor the encryptor used to decrypt the accessToken, secret and refreshToken (optional) * * @return the map as {@link ConnectionData} */ public static ConnectionData mapToConnectionData(String providerId, Map<String, Object> map, TextEncryptor encryptor) throws CryptoException { String providerUserId = (String) map.get("providerUserId"); String displayName = (String) map.get("displayName"); String profileUrl = (String) map.get("profileUrl"); String imageUrl = (String) map.get("imageUrl"); String accessToken = decrypt((String)map.get("accessToken"), encryptor); String secret = decrypt((String)map.get("secret"), encryptor); String refreshToken = decrypt((String)map.get("refreshToken"), encryptor); Long expireTime = (Long) map.get("expireTime"); return new ConnectionData(providerId, providerUserId, displayName, profileUrl, imageUrl, accessToken, secret, refreshToken, expireTime); } /** * Adds the specified {@link ConnectionData} to the profile. If a connection * data with the same user ID already exists, it will be replaced with the new data. * * @param profile the profile * @param connectionData the connection data to add * @param encryptor the encryptor used to encrypt the accessToken, secret and refreshToken */ public static void addConnectionData(Profile profile, ConnectionData connectionData, TextEncryptor encryptor) throws CryptoException { Map<String, List<Map<String, Object>>> allConnections = profile.getAttribute(CONNECTIONS_ATTRIBUTE_NAME); List<Map<String, Object>> connectionsForProvider = null; if (allConnections == null) { allConnections = new HashMap<>(); profile.setAttribute(CONNECTIONS_ATTRIBUTE_NAME, allConnections); } else { connectionsForProvider = allConnections.get(connectionData.getProviderId()); } if (connectionsForProvider == null) { connectionsForProvider = new ArrayList<>(); allConnections.put(connectionData.getProviderId(), connectionsForProvider); } Map<String, Object> currentConnectionDataMap = null; for (Map<String, Object> connectionDataMap : connectionsForProvider) { if (connectionData.getProviderUserId().equals(connectionDataMap.get("providerUserId"))) { currentConnectionDataMap = connectionDataMap; break; } } if (currentConnectionDataMap != null) { currentConnectionDataMap.putAll(connectionDataToMap(connectionData, encryptor)); } else { connectionsForProvider.add(connectionDataToMap(connectionData, encryptor)); } } /** * Returns the list of {@link ConnectionData} associated to the provider ID of * the specified profile * * @param profile the profile that contains the connection data in its attributes * @param providerId the provider ID of the connection * @param encryptor the encryptor used to decrypt the accessToken, secret and refreshToken * * @return the list of connection data for the provider, or empty if no connection data was found */ public static List<ConnectionData> getConnectionData(Profile profile, String providerId, TextEncryptor encryptor) throws CryptoException { Map<String, List<Map<String, Object>>> allConnections = profile.getAttribute(CONNECTIONS_ATTRIBUTE_NAME); if (MapUtils.isNotEmpty(allConnections)) { List<Map<String, Object>> connectionsForProvider = allConnections.get(providerId); if (CollectionUtils.isNotEmpty(connectionsForProvider)) { List<ConnectionData> connectionDataList = new ArrayList<>(connectionsForProvider.size()); for (Map<String, Object> connectionDataMap : connectionsForProvider) { connectionDataList.add(mapToConnectionData(providerId, connectionDataMap, encryptor)); } return connectionDataList; } } return Collections.emptyList(); } /** * Remove all {@link ConnectionData} associated to the specified provider ID. * * @param profile the profile where to remove the data from * @param providerId the provider ID of the connection */ public static void removeConnectionData(Profile profile, String providerId) { Map<String, List<Map<String, Object>>> allConnections = profile.getAttribute(CONNECTIONS_ATTRIBUTE_NAME); if (MapUtils.isNotEmpty(allConnections)) { allConnections.remove(providerId); } } /** * Remove the {@link ConnectionData} associated to the provider ID and user ID. * * @param providerId the provider ID of the connection * @param providerUserId the provider user ID * @param profile the profile where to remove the data from */ public static void removeConnectionData(String providerId, String providerUserId, Profile profile) { Map<String, List<Map<String, Object>>> allConnections = profile.getAttribute(CONNECTIONS_ATTRIBUTE_NAME); if (MapUtils.isNotEmpty(allConnections)) { List<Map<String, Object>> connectionsForProvider = allConnections.get(providerId); if (CollectionUtils.isNotEmpty(connectionsForProvider)) { for (Iterator<Map<String, Object>> iter = connectionsForProvider.iterator(); iter.hasNext();) { Map<String, Object> connectionDataMap = iter.next(); if (providerUserId.equals(connectionDataMap.get("providerUserId"))) { iter.remove(); } } } } } /** * Adds the info from the provider profile to the specified profile. * * @param profile the target profile * @param providerProfile the provider profile where to get the info */ public static void addProviderProfileInfo(Profile profile, UserProfile providerProfile) { String email = providerProfile.getEmail(); if (StringUtils.isEmpty(email)) { throw new IllegalStateException("No email included in provider profile"); } String username = providerProfile.getUsername(); if (StringUtils.isEmpty(username)) { username = email; } String firstName = providerProfile.getFirstName(); String lastName = providerProfile.getLastName(); profile.setUsername(username); profile.setEmail(email); profile.setAttribute(FIRST_NAME_ATTRIBUTE_NAME, firstName); profile.setAttribute(LAST_NAME_ATTRIBUTE_NAME, lastName); } /** * Creates a profile from the specified connection. * * @param connection the connection where to retrieve the profile info from * * @return */ public static Profile createProfile(Connection<?> connection) { Profile profile = new Profile(); UserProfile providerProfile = connection.fetchUserProfile(); String email = providerProfile.getEmail(); if (StringUtils.isEmpty(email)) { throw new IllegalStateException("No email included in provider profile"); } String username = providerProfile.getUsername(); if (StringUtils.isEmpty(username)) { username = email; } String firstName = providerProfile.getFirstName(); String lastName = providerProfile.getLastName(); String displayName; if (StringUtils.isNotEmpty(connection.getDisplayName())) { displayName = connection.getDisplayName(); } else { displayName = firstName + " " + lastName; } profile.setUsername(username); profile.setEmail(email); profile.setAttribute(FIRST_NAME_ATTRIBUTE_NAME, firstName); profile.setAttribute(LAST_NAME_ATTRIBUTE_NAME, lastName); profile.setAttribute(DISPLAY_NAME_ATTRIBUTE_NAME, displayName); if (StringUtils.isNotEmpty(connection.getImageUrl())) { profile.setAttribute(AVATAR_LINK_ATTRIBUTE_NAME, connection.getImageUrl()); } return profile; } private static String encrypt(String clear, TextEncryptor encryptor) throws CryptoException { return encryptor != null && StringUtils.isNotEmpty(clear) ? encryptor.encrypt(clear) : clear; } private static String decrypt(String encrypted, TextEncryptor encryptor) throws CryptoException { return encryptor != null && StringUtils.isNotEmpty(encrypted) ? encryptor.decrypt(encrypted) : encrypted; } }