Spring boot User Registration and OAuth2 Social login with Facebook, Google, LinkedIn and Github – Part 1

What you’ll build

Login

Register

Home

What you’ll need

  • Spring Tool Suite 4
  • JDK 11
  • MySQL Server 8

Tech Stack

  • Spring Boot 2 and Spring Security 5
  • Spring Data JPA and Hibernate 5
  • JSP and Bootstrap 4

Configure Google, Facebook, Github and LinkedIn for Social Login in Your Spring Boot App

Facebook

Here are the steps you need to follow to configure Facebook for social login:

  • Go to https://developers.facebook.com and register for a developer account, if you haven’t already done so.
  • Head over to the Facebook app dashboard: https://developers.facebook.com/apps.
  • Create a Facebook app. Instructions for creating a Facebook application can be found here: https://developers.facebook.com/docs/apps/register.
  • Once you’ve created your Facebook app, go to the app dashboard, click the Settings link on the left-hand side, and select the Basic submenu.
  • Save the App ID and App Secret values. You’ll need them later.
  • You’ll also need to add a Facebook Login product. From the left menu, click the + sign next to products and add a Facebook Login product.
  • Fill in the Authorized redirect URIs field to include the redirect URI to your app: http://<your-domain>/login/oauth2/code/facebook
  • Save changes

Note: To work with localhost, The facebook app should be in development mode and ” Valid OAuth Redirect URIs” should be blank as shown below:

Google

Here are the steps you need to follow to configure Google for social login:

  • Go to https://console.developers.google.com/ and register for a developer account.
  • Create a Google API Console project.
  • Once your Google App is open, click on the Credentials menu and then Create Credentials followed by OAuth client ID.
  • Select Web Application as the Application type.
  • Give the client a name.
  • Fill in the Authorized redirect URIs field to include the redirect URI to your app: http://<your-domain>/login/oauth2/code/google
  • Click Create.
  • Copy the client ID and client secret, as you’ll need them later.

Github

Here are the steps you need to follow to configure Github for social login:

  • Go to  https://github.com/settings/developers and create a New OAuth app under the OAuth Apps left menu.
  • Fill in the Authorization callback URL field to include the redirect URI to your app: http://<your-domain>/login/oauth2/code/github
  • Click on Register Application
  • Copy the client ID and client secret, as you’ll need them later.

LinkedIn

Here are the steps you need to follow to configure LinkedIn for social login:

  • Go to  https://www.linkedin.com/developers/apps and create a new app.
  • Go to My Apps and select the created app.
  • Copy the client ID and client secret, as you’ll need them later.
  • Click on the Auth tab and Fill in the Redirect URLs under OAuth 2.0 settings to include the redirect URI to your app: http://<your-domain>/login/oauth2/code/linkedin

Bootstrap your application

You can create your spring boot application with the required dependencies and download it from here

Project Structure

|---pom.xml
|   
+---src
    +---main
        +---java
        |   \---com
        |       \---javachinna
        |           |   DemoApplication.java
        |           |   
        |           +---config
        |           |       AppConfig.java
        |           |       SetupDataLoader.java
        |           |       WebSecurityConfig.java
        |           |       
        |           +---controller
        |           |       PagesController.java
        |           |       
        |           +---dto
        |           |       LocalUser.java
        |           |       SocialProvider.java
        |           |       UserRegistrationForm.java
        |           |       
        |           +---exception
        |           |       OAuth2AuthenticationProcessingException.java
        |           |       UserAlreadyExistAuthenticationException.java
        |           |       
        |           +---handler
        |           |       CustomAuthenticationFailureHandler.java
        |           |       CustomLogoutSuccessHandler.java
        |           |       
        |           +---model
        |           |       Role.java
        |           |       User.java
        |           |       
        |           +---oauth2
        |                   CustomOAuth2UserService.java
        |                   CustomOidcUserService.java
        |                   OAuth2AccessTokenResponseConverterWithDefaults.java
        |                   
        |           |   \---user
        |           |           FacebookOAuth2UserInfo.java
        |           |           GithubOAuth2UserInfo.java
        |           |           GoogleOAuth2UserInfo.java
        |           |           LinkedinOAuth2UserInfo.java
        |           |           OAuth2UserInfo.java
        |           |           OAuth2UserInfoFactory.java
        |           |           
        |           +---repo
        |           |       RoleRepository.java
        |           |       UserRepository.java
        |           |       
        |           +---service
        |           |       LocalUserDetailService.java
        |           |       MessageService.java
        |           |       UserService.java
        |           |       UserServiceImpl.java
        |           |       
        |           +---util
        |           |       GeneralUtils.java
        |           |       
        |           \---validator
        |                   PasswordMatches.java
        |                   PasswordMatchesValidator.java
        |                   
        +---resources
        |       application.properties
        |       messages_en.properties
        |       
        \---webapp
            +---static
            |   +---css
                        style.css
                        
            |   \---img
            |           facebook.png
            |           github.png
            |           google.png
            |           linkedin.png
            |           
            \---WEB-INF
                \---views
                    \---pages
                            home.jsp
                            login.jsp
                            register.jsp

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.8.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javachinna</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>

		<!-- To compile JSP files -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-taglibs</artifactId>
		</dependency>
		<!-- mysql driver -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>bootstrap</artifactId>
			<version>4.1.1</version>
		</dependency>
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>webjars-locator</artifactId>
			<version>0.36</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Define JPA Entities and Repositories

@Entity annotation specifies that the class is an entity.

@Id annotation is used to specify the identifier property of the entity bean. 

@GeneratedValue annotation is used to generate the primary key value automatically. This can use 4 generation types: AUTO, IDENTITY, SEQUENCE, TABLE. If we don’t specify a value explicitly, the generation type defaults to AUTO.

@Column annotation is used to map the field to the database table column

User.java

@Entity
public class User implements Serializable {
	private static final long serialVersionUID = 65981149772133526L;
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Long id;

	@Column(name = "PROVIDER_USER_ID")
	private String providerUserId;

	private String email;

	@Column(name = "enabled", columnDefinition = "BIT", length = 1)
	private boolean enabled;

	@Column(name = "DISPLAY_NAME")
	private String displayName;

	@Column(name = "created_date", nullable = false, updatable = false)
	@Temporal(TemporalType.TIMESTAMP)
	protected Date createdDate;

	@Temporal(TemporalType.TIMESTAMP)
	protected Date modifiedDate;

	private String password;

	private String provider;

	// bi-directional many-to-many association to Role
	@JsonIgnore
	@ManyToMany
	@JoinTable(name = "user_role", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") })
	private Set<Role> roles;

	public User() {
		this.enabled = false;
	}

	// Getters and setters goes here
}

Role.java

@Entity
public class Role implements Serializable {
	private static final long serialVersionUID = 1L;
	public static final String USER = "USER";
	public static final String ROLE_USER = "ROLE_USER";

	@Id
	@Column(name = "ROLE_ID")
	private int roleId;

	private String name;

	// bi-directional many-to-many association to User
	@ManyToMany(mappedBy = "roles")
	private Set<User> users;

	public Role() {
	}

	public Role(String name) {
		this.name = name;
	}

	// Getters and setters goes here

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final Role role = (Role) obj;
		if (!role.equals(role.name)) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("Role [name=").append(name).append("]").append("[id=").append(roleId).append("]");
		return builder.toString();
	}
}

@Table maps the entity with the table. If no @Table is defined, the default value is used: the class name of the entity.

@ManyToMany defines a many-to-many relationship between 2 entities.

mappedBy indicates the entity is the inverse of the relationship.

Spring Data JPA Repositories

Spring Data JPA contains some built-in Repository implemented common functions to work with database such as findOnefindAll and save. We just need to extend the JpaRepository interface and define our custom methods.

UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
	User findByEmail(String email);
	boolean existsByEmail(String email);
}

The query builder mechanism built into Spring Data repository infrastructure strips the prefixes find…Byread…Byquery…Bycount…Byexists…By , and get…By from the method and starts parsing the rest of it to frame the query. For more information go through Spring Data JPA Documentation.

RoleRepository.java

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
	Role findByName(String name);
}

Creating an utility class

GeneralUtils.java

public class GeneralUtils {

	public static List<SimpleGrantedAuthority> buildSimpleGrantedAuthorities(final Set<Role> roles) {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		for (Role role : roles) {
			authorities.add(new SimpleGrantedAuthority(role.getName()));
		}
		return authorities;
	}

	public static SocialProvider toSocialProvider(String providerId) {
		for (SocialProvider socialProvider : SocialProvider.values()) {
			if (socialProvider.getProviderType().equals(providerId)) {
				return socialProvider;
			}
		}
		return SocialProvider.LOCAL;
	}
}

Creating service layer to access the repositories

UserService.java

public interface UserService {
	public User registerNewUser(UserRegistrationForm UserRegistrationForm) throws UserAlreadyExistAuthenticationException;
	User findUserByEmail(String email);
	LocalUser processUserRegistration(String registrationId, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo);
}


UserServiceImpl.java

@Service annotation indicates that annotated class is a “Service”

@Autowired annotation is used for automatic dependency injection.

@Qualifier annotation is used when you create more than one bean of the same type and want to wire only one of them with a property. In such cases, you can use this  annotation along with @Autowired to remove the confusion by specifying which exact bean will be wired.

Spring creates proxies for all the classes annotated with @Transactional  – either on the class or on any of the methods. The proxy allows the framework to inject transactional logic before and after the running method – mainly for starting and committing the transaction.

@Service
public class UserServiceImpl implements UserService {

	@Autowired
	@Qualifier(value = "localUserDetailService")
	private UserDetailsService userDetailService;

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private RoleRepository roleRepository;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Override
	@Transactional(value = "transactionManager")
	public User registerNewUser(final UserRegistrationForm userRegistrationForm) throws UserAlreadyExistAuthenticationException {
		if (userRegistrationForm.getUserID() != null &amp;&amp; userRepository.existsById(userRegistrationForm.getUserID())) {
			throw new UserAlreadyExistAuthenticationException("User with User id " + userRegistrationForm.getUserID() + " already exist");
		} else if (userRepository.existsByEmail(userRegistrationForm.getEmail())) {
			throw new UserAlreadyExistAuthenticationException("User with email id " + userRegistrationForm.getEmail() + " already exist");
		}
		User user = buildUser(userRegistrationForm);
		Date now = Calendar.getInstance().getTime();
		user.setCreatedDate(now);
		user.setModifiedDate(now);
		user = userRepository.save(user);
		userRepository.flush();
		return user;
	}

	private User buildUser(final UserRegistrationForm formDTO) {
		User user = new User();
		user.setDisplayName(formDTO.getDisplayName());
		user.setEmail(formDTO.getEmail());
		user.setPassword(passwordEncoder.encode(formDTO.getPassword()));
		final HashSet<Role> roles = new HashSet<Role>();
		roles.add(roleRepository.findByName(Role.ROLE_USER));
		user.setRoles(roles);
		user.setProvider(formDTO.getSocialProvider().getProviderType());
		user.setEnabled(true);
		user.setProviderUserId(formDTO.getProviderUserId());
		return user;
	}

	@Override
	public User findUserByEmail(final String email) {
		return userRepository.findByEmail(email);
	}

	@Override
	@Transactional
	public LocalUser processUserRegistration(String registrationId, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo) {
		OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, attributes);
		if (StringUtils.isEmpty(oAuth2UserInfo.getName())) {
			throw new OAuth2AuthenticationProcessingException("Name not found from OAuth2 provider");
		} else if (StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
			throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
		}
		UserRegistrationForm userDetails = toUserRegistrationObject(registrationId, oAuth2UserInfo);
		User user = findUserByEmail(oAuth2UserInfo.getEmail());
		if (user != null) {
			if (!user.getProvider().equals(registrationId) &amp;&amp; !user.getProvider().equals(SocialProvider.LOCAL.getProviderType())) {
				throw new OAuth2AuthenticationProcessingException(
						"Looks like you're signed up with " + user.getProvider() + " account. Please use your " + user.getProvider() + " account to login.");
			}
			user = updateExistingUser(user, oAuth2UserInfo);
		} else {
			user = registerNewUser(userDetails);
		}

		return LocalUser.create(user, attributes, idToken, userInfo);
	}

	private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
		existingUser.setDisplayName(oAuth2UserInfo.getName());
		return userRepository.save(existingUser);
	}

	private UserRegistrationForm toUserRegistrationObject(String registrationId, OAuth2UserInfo oAuth2UserInfo) {
		return UserRegistrationForm.getBuilder().addProviderUserID(oAuth2UserInfo.getId()).addDisplayName(oAuth2UserInfo.getName()).addEmail(oAuth2UserInfo.getEmail())
				.addSocialProvider(GeneralUtils.toSocialProvider(registrationId)).addPassword("changeit").build();
	}
}

Note: For users who signed up using a social login provider, you can provide a link to reset the password once they logged in. This way they will be able to login using their credentials as well.

LocalUserDetailService.java

@Service("localUserDetailService")
public class LocalUserDetailService implements UserDetailsService {
	@Autowired
	private UserService userService;

	@Override
	@Transactional
	public LocalUser loadUserByUsername(final String email) throws UsernameNotFoundException {
		User user = userService.findUserByEmail(email);
		if (user == null) {
			throw new UsernameNotFoundException("User " + email + " was not found in the database");
		}
		return new LocalUser(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, GeneralUtils.buildSimpleGrantedAuthorities(user.getRoles()), user);
	}
}

What’s next?

In this article, we have covered social login configuration, created data access layer and service layer. In the next article, we’ll develop the view layer and configure Spring security

Read NextSpring boot User Registration and OAuth2 Social login with Facebook, Google, LinkedIn and Github – Part 2

This Post Has 2 Comments

  1. Pankaj kumar Jha

    hlo sir gud mng its superb blog i ever seen …. is this social login and registration is using with open id?
    and sir how to login social by using open id connect?
    plz explain sir

    1. Chinna

      Hi Pankaj,

      Good afternoon. Thank you for your comments.

      Google is an OpenID Connect provider. Thats why we have created CustomOidcUserService in Part 3 to implement social login with Google. You can refer the same to implement social login with other OpenID Connect providers too. Facebook, LinkedIn and Github are OAuth2 login providers.

      Please let me know if more clarifications required.

Leave a Reply