In my previous post, I talked about the first challenge I had was to change the data model and add the connection framework. Here I want to give more details about how I did it. Spring Social project already provides a jdbc based connection repository implementation to persist user connection data into a relational database. However, I’m using MongoDB, so I need to customize the code, and I found it is relatively easy to do it. The user connection data will be saved as the object of UserSocialConnection
, it is a MongoDB document:
@SuppressWarnings('serial')
@Document(collection = 'UserSocialConnection')
public class UserSocialConnection extends BaseEntity {
private String userId;
private String providerId;
private String providerUserId;
private String displayName;
private String profileUrl;
private String imageUrl;
private String accessToken;
private String secret;
private String refreshToken;
private Long expireTime;
//Getter/Setter omitted.
public UserSocialConnection() {
super();
}
public UserSocialConnection(String userId, String providerId, String providerUserId, int rank,
String displayName, String profileUrl, String imageUrl, String accessToken, String secret,
String refreshToken, Long expireTime) {
super();
this.userId = userId;
this.providerId = providerId;
this.providerUserId = providerUserId;
this.displayName = displayName;
this.profileUrl = profileUrl;
this.imageUrl = imageUrl;
this.accessToken = accessToken;
this.secret = secret;
this.refreshToken = refreshToken;
this.expireTime = expireTime;
}
}
BaseEntity
just has ‘id’. With the help of the Spring Data project, I don’t need to write any code of CRUD operations for UserSocialConnection
, just extend MongoRepository
:
public interface UserSocialConnectionRepository extends MongoRepository<UserSocialConnection, String>{
List<UserSocialConnection> findByUserId(String userId);
List<UserSocialConnection> findByUserIdAndProviderId(String userId, String providerId);
List<UserSocialConnection> findByProviderIdAndProviderUserId(String providerId, String providerUserId);
UserSocialConnection findByUserIdAndProviderIdAndProviderUserId(String userId, String providerId, String providerUserId);
List<UserSocialConnection> findByProviderIdAndProviderUserIdIn(String providerId, Collection<String> providerUserIds);
}
After we have our database UserSocialConnectionRepository
, we will implement Spring Social required ConnectionRepository
and UsersConnectionRepository
. I just copied the code from JdbcConnectionRepository
and JdbcUsersConnectionRepository
, and created my own MongoConnectionRepository
and MongoUsersConnectionRepository
.
public class MongoUsersConnectionRepository implements UsersConnectionRepository{
private final UserSocialConnectionRepository userSocialConnectionRepository;
private final SocialAuthenticationServiceLocator socialAuthenticationServiceLocator;
private final TextEncryptor textEncryptor;
private ConnectionSignUp connectionSignUp;
public MongoUsersConnectionRepository(UserSocialConnectionRepository userSocialConnectionRepository,
SocialAuthenticationServiceLocator socialAuthenticationServiceLocator, TextEncryptor textEncryptor){
this.userSocialConnectionRepository = userSocialConnectionRepository;
this.socialAuthenticationServiceLocator = socialAuthenticationServiceLocator;
this.textEncryptor = textEncryptor;
}
/**
* The command to execute to create a new local user profile in the event no user id could be mapped to a connection.
* Allows for implicitly creating a user profile from connection data during a provider sign-in attempt.
* Defaults to null, indicating explicit sign-up will be required to complete the provider sign-in attempt.
* @see #findUserIdsWithConnection(Connection)
*/
public void setConnectionSignUp(ConnectionSignUp connectionSignUp) {
this.connectionSignUp = connectionSignUp;
}
public List<String> findUserIdsWithConnection(Connection<?> connection) {
ConnectionKey key = connection.getKey();
List<UserSocialConnection> userSocialConnectionList =
this.userSocialConnectionRepository.findByProviderIdAndProviderUserId(key.getProviderId(), key.getProviderUserId());
List<String> localUserIds = new ArrayList<String>();
for (UserSocialConnection userSocialConnection : userSocialConnectionList){
localUserIds.add(userSocialConnection.getUserId());
}
if (localUserIds.size() == 0 && connectionSignUp != null) {
String newUserId = connectionSignUp.execute(connection);
if (newUserId != null)
{
createConnectionRepository(newUserId).addConnection(connection);
return Arrays.asList(newUserId);
}
}
return localUserIds;
}
public Set<String> findUserIdsConnectedTo(String providerId, Set<String> providerUserIds) {
final Set<String> localUserIds = new HashSet<String>();
List<UserSocialConnection> userSocialConnectionList =
this.userSocialConnectionRepository.findByProviderIdAndProviderUserIdIn(providerId, providerUserIds);
for (UserSocialConnection userSocialConnection : userSocialConnectionList){
localUserIds.add(userSocialConnection.getUserId());
}
return localUserIds;
}
public ConnectionRepository createConnectionRepository(String userId) {
if (userId == null) {
throw new IllegalArgumentException('userId cannot be null');
}
return new MongoConnectionRepository(userId, userSocialConnectionRepository, socialAuthenticationServiceLocator, textEncryptor);
}
}
MongoUsersConnectionRepository
is pretty much exactly like JdbcUsersConnectionRepository
. But for MongoConnectionRepository
, I needed to make some changes:
public class MongoConnectionRepository implements ConnectionRepository {
private final String userId;
private final UserSocialConnectionRepository userSocialConnectionRepository;
private final SocialAuthenticationServiceLocator socialAuthenticationServiceLocator;
private final TextEncryptor textEncryptor;
public MongoConnectionRepository(String userId, UserSocialConnectionRepository userSocialConnectionRepository,
SocialAuthenticationServiceLocator socialAuthenticationServiceLocator, TextEncryptor textEncryptor) {
this.userId = userId;
this.userSocialConnectionRepository = userSocialConnectionRepository;
this.socialAuthenticationServiceLocator = socialAuthenticationServiceLocator;
this.textEncryptor = textEncryptor;
}
public MultiValueMap<String, Connection<?>> findAllConnections() {
List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
.findByUserId(userId);
MultiValueMap<String, Connection<?>> connections = new LinkedMultiValueMap<String, Connection<?>>();
Set<String> registeredProviderIds = socialAuthenticationServiceLocator.registeredProviderIds();
for (String registeredProviderId : registeredProviderIds) {
connections.put(registeredProviderId, Collections.<Connection<?>> emptyList());
}
for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
String providerId = userSocialConnection.getProviderId();
if (connections.get(providerId).size() == 0) {
connections.put(providerId, new LinkedList<Connection<?>>());
}
connections.add(providerId, buildConnection(userSocialConnection));
}
return connections;
}
public List<Connection<?>> findConnections(String providerId) {
List<Connection<?>> resultList = new LinkedList<Connection<?>>();
List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
.findByUserIdAndProviderId(userId, providerId);
for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
resultList.add(buildConnection(userSocialConnection));
}
return resultList;
}
@SuppressWarnings('unchecked')
public <A> List<Connection<A>> findConnections(Class<A> apiType) {
List<?> connections = findConnections(getProviderId(apiType));
return (List<Connection<A>>) connections;
}
public MultiValueMap<String, Connection<?>> findConnectionsToUsers(MultiValueMap<String, String> providerUsers) {
if (providerUsers == null || providerUsers.isEmpty()) {
throw new IllegalArgumentException('Unable to execute find: no providerUsers provided');
}
MultiValueMap<String, Connection<?>> connectionsForUsers = new LinkedMultiValueMap<String, Connection<?>>();
for (Iterator<Entry<String, List<String>>> it = providerUsers.entrySet().iterator(); it.hasNext();) {
Entry<String, List<String>> entry = it.next();
String providerId = entry.getKey();
List<String> providerUserIds = entry.getValue();
List<UserSocialConnection> userSocialConnections =
this.userSocialConnectionRepository.findByProviderIdAndProviderUserIdIn(providerId, providerUserIds);
List<Connection<?>> connections = new ArrayList<Connection<?>>(providerUserIds.size());
for (int i = 0; i < providerUserIds.size(); i++) {
connections.add(null);
}
connectionsForUsers.put(providerId, connections);
for (UserSocialConnection userSocialConnection : userSocialConnections) {
String providerUserId = userSocialConnection.getProviderUserId();
int connectionIndex = providerUserIds.indexOf(providerUserId);
connections.set(connectionIndex, buildConnection(userSocialConnection));
}
}
return connectionsForUsers;
}
public Connection<?> getConnection(ConnectionKey connectionKey) {
UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
.findByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(),
connectionKey.getProviderUserId());
if (userSocialConnection != null) {
return buildConnection(userSocialConnection);
}
throw new NoSuchConnectionException(connectionKey);
}
@SuppressWarnings('unchecked')
public <A> Connection<A> getConnection(Class<A> apiType, String providerUserId) {
String providerId = getProviderId(apiType);
return (Connection<A>) getConnection(new ConnectionKey(providerId, providerUserId));
}
@SuppressWarnings('unchecked')
public <A> Connection<A> getPrimaryConnection(Class<A> apiType) {
String providerId = getProviderId(apiType);
Connection<A> connection = (Connection<A>) findPrimaryConnection(providerId);
if (connection == null) {
throw new NotConnectedException(providerId);
}
return connection;
}
@SuppressWarnings('unchecked')
public <A> Connection<A> findPrimaryConnection(Class<A> apiType) {
String providerId = getProviderId(apiType);
return (Connection<A>) findPrimaryConnection(providerId);
}
public void addConnection(Connection<?> connection) {
//check cardinality
SocialAuthenticationService<?> socialAuthenticationService =
this.socialAuthenticationServiceLocator.getAuthenticationService(connection.getKey().getProviderId());
if (socialAuthenticationService.getConnectionCardinality() == ConnectionCardinality.ONE_TO_ONE ||
socialAuthenticationService.getConnectionCardinality() == ConnectionCardinality.ONE_TO_MANY){
List<UserSocialConnection> storedConnections =
this.userSocialConnectionRepository.findByProviderIdAndProviderUserId(
connection.getKey().getProviderId(), connection.getKey().getProviderUserId());
if (storedConnections.size() > 0){
//not allow one providerId connect to multiple userId
throw new DuplicateConnectionException(connection.getKey());
}
}
UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
.findByUserIdAndProviderIdAndProviderUserId(userId, connection.getKey().getProviderId(),
connection.getKey().getProviderUserId());
if (userSocialConnection == null) {
ConnectionData data = connection.createData();
userSocialConnection = new UserSocialConnection(userId, data.getProviderId(), data.getProviderUserId(), 0,
data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()),
encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime());
this.userSocialConnectionRepository.save(userSocialConnection);
} else {
throw new DuplicateConnectionException(connection.getKey());
}
}
public void updateConnection(Connection<?> connection) {
ConnectionData data = connection.createData();
UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
.findByUserIdAndProviderIdAndProviderUserId(userId, connection.getKey().getProviderId(), connection
.getKey().getProviderUserId());
if (userSocialConnection != null) {
userSocialConnection.setDisplayName(data.getDisplayName());
userSocialConnection.setProfileUrl(data.getProfileUrl());
userSocialConnection.setImageUrl(data.getImageUrl());
userSocialConnection.setAccessToken(encrypt(data.getAccessToken()));
userSocialConnection.setSecret(encrypt(data.getSecret()));
userSocialConnection.setRefreshToken(encrypt(data.getRefreshToken()));
userSocialConnection.setExpireTime(data.getExpireTime());
this.userSocialConnectionRepository.save(userSocialConnection);
}
}
public void removeConnections(String providerId) {
List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
.findByUserIdAndProviderId(userId, providerId);
for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
this.userSocialConnectionRepository.delete(userSocialConnection);
}
}
public void removeConnection(ConnectionKey connectionKey) {
UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
.findByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(), connectionKey.getProviderUserId());
this.userSocialConnectionRepository.delete(userSocialConnection);
}
// internal helpers
private Connection<?> buildConnection(UserSocialConnection userSocialConnection) {
ConnectionData connectionData = new ConnectionData(userSocialConnection.getProviderId(),
userSocialConnection.getProviderUserId(), userSocialConnection.getDisplayName(),
userSocialConnection.getProfileUrl(), userSocialConnection.getImageUrl(),
decrypt(userSocialConnection.getAccessToken()), decrypt(userSocialConnection.getSecret()),
decrypt(userSocialConnection.getRefreshToken()), userSocialConnection.getExpireTime());
ConnectionFactory<?> connectionFactory = this.socialAuthenticationServiceLocator.getConnectionFactory(connectionData
.getProviderId());
return connectionFactory.createConnection(connectionData);
}
private Connection<?> findPrimaryConnection(String providerId) {
List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
.findByUserIdAndProviderId(userId, providerId);
return buildConnection(userSocialConnectionList.get(0));
}
private <A> String getProviderId(Class<A> apiType) {
return socialAuthenticationServiceLocator.getConnectionFactory(apiType).getProviderId();
}
private String encrypt(String text) {
return text != null ? textEncryptor.encrypt(text) : text;
}
private String decrypt(String encryptedText) {
return encryptedText != null ? textEncryptor.decrypt(encryptedText) : encryptedText;
}
}
First, I replaced JdbcTemplate
with UserSocialConnectionRepository
to retrieve UserSocialConnection objects from the database. Then replaced ConnectionFactoryLocator
with SocialAuthenticationServiceLocator
from spring-social-security module. A big change is in the addConnection
method (highlighted above), where it checks connection cardinality first. If connectionCardinality
of socialAuthenticationService
is ONE_TO_ONE
(which means one userId with one and only one pair of providerId/providerUserId), or ONE_TO_MANY
(which means one userId can connect to one or many providerId/providerUserId, but one pair of providerId/providerUserId can only connect to one userId).
After all those customizations, the final step is to glue them together in spring config:
@Configuration
public class SocialAndSecurityConfig {
@Inject
private Environment environment;
@Inject
AccountService accountService;
@Inject
private AuthenticationManager authenticationManager;
@Inject
private UserSocialConnectionRepository userSocialConnectionRepository;
@Bean
public SocialAuthenticationServiceLocator socialAuthenticationServiceLocator() {
SocialAuthenticationServiceRegistry registry = new SocialAuthenticationServiceRegistry();
//add google
OAuth2ConnectionFactory<Google> googleConnectionFactory = new GoogleConnectionFactory(environment.getProperty('google.clientId'),
environment.getProperty('google.clientSecret'));
OAuth2AuthenticationService<Google> googleAuthenticationService = new OAuth2AuthenticationService<Google>(googleConnectionFactory);
googleAuthenticationService.setScope('https://www.googleapis.com/auth/userinfo.profile');
registry.addAuthenticationService(googleAuthenticationService);
//add twitter
OAuth1ConnectionFactory<Twitter> twitterConnectionFactory = new TwitterConnectionFactory(environment.getProperty('twitter.consumerKey'),
environment.getProperty('twitter.consumerSecret'));
OAuth1AuthenticationService<Twitter> twitterAuthenticationService = new OAuth1AuthenticationService<Twitter>(twitterConnectionFactory);
registry.addAuthenticationService(twitterAuthenticationService);
//add facebook
OAuth2ConnectionFactory<Facebook> facebookConnectionFactory = new FacebookConnectionFactory(environment.getProperty('facebook.clientId'),
environment.getProperty('facebook.clientSecret'));
OAuth2AuthenticationService<Facebook> facebookAuthenticationService = new OAuth2AuthenticationService<Facebook>(facebookConnectionFactory);
facebookAuthenticationService.setScope('');
registry.addAuthenticationService(facebookAuthenticationService);
return registry;
}
/**
* Singleton data access object providing access to connections across all users.
*/
@Bean
public UsersConnectionRepository usersConnectionRepository() {
MongoUsersConnectionRepository repository = new MongoUsersConnectionRepository(userSocialConnectionRepository,
socialAuthenticationServiceLocator(), Encryptors.noOpText());
repository.setConnectionSignUp(autoConnectionSignUp());
return repository;
}
/**
* Request-scoped data access object providing access to the current user's connections.
*/
@Bean
@Scope(value = 'request', proxyMode = ScopedProxyMode.INTERFACES)
public ConnectionRepository connectionRepository() {
UserAccount user = AccountUtils.getLoginUserAccount();
return usersConnectionRepository().createConnectionRepository(user.getUsername());
}
/**
* A proxy to a request-scoped object representing the current user's primary Google account.
*
* @throws NotConnectedException
* if the user is not connected to Google.
*/
@Bean
@Scope(value = 'request', proxyMode = ScopedProxyMode.INTERFACES)
public Google google() {
Connection<Google> google = connectionRepository().findPrimaryConnection(Google.class);
return google != null ? google.getApi() : new GoogleTemplate();
}
@Bean
@Scope(value='request', proxyMode=ScopedProxyMode.INTERFACES)
public Facebook facebook() {
Connection<Facebook> facebook = connectionRepository().findPrimaryConnection(Facebook.class);
return facebook != null ? facebook.getApi() : new FacebookTemplate();
}
@Bean
@Scope(value='request', proxyMode=ScopedProxyMode.INTERFACES)
public Twitter twitter() {
Connection<Twitter> twitter = connectionRepository().findPrimaryConnection(Twitter.class);
return twitter != null ? twitter.getApi() : new TwitterTemplate();
}
@Bean
public ConnectionSignUp autoConnectionSignUp() {
return new AutoConnectionSignUp(accountService);
}
@Bean
public SocialAuthenticationFilter socialAuthenticationFilter() {
SocialAuthenticationFilter filter = new SocialAuthenticationFilter(authenticationManager, accountService,
usersConnectionRepository(), socialAuthenticationServiceLocator());
filter.setFilterProcessesUrl('/signin');
filter.setSignupUrl(null);
filter.setConnectionAddedRedirectUrl('/myAccount');
filter.setPostLoginUrl('/myAccount');
return filter;
}
@Bean
public SocialAuthenticationProvider socialAuthenticationProvider(){
return new SocialAuthenticationProvider(usersConnectionRepository(), accountService);
}
@Bean
public LoginUrlAuthenticationEntryPoint socialAuthenticationEntryPoint(){
return new LoginUrlAuthenticationEntryPoint('/signin');
}
}
accountService
is my own user account service to provide account related functions, and it implements SocialUserDetailsService
, UserDetailsService
, UserIdExtractor
.
There are still many areas to improve, such as refactoring MongoConnectionRepository
and MongoUsersConnectionRepository
to have an abstract social connection repository implementation using Spring Data Repository interface. And I found someone already has raised an issue for that: Leverage Spring Data for UsersConnectionRepository.
Reference: Customize Spring Social Connect Framework For MongoDB from our JCG partner Yuan Ji at the Jiwhiz blog.