How to Build Spring Boot Angular User Registration and OAuth2 Social Login with Facebook, Google, LinkedIn, and Github – Part 2

Welcome to the 2nd part of the Spring Boot 2 Angular 10 OAuth2 Social Login tutorial series. In this previous article, we have implemented Data Access Layer, Service Layer, Validation, and Exception Handling. In this article, we are going to implement User registration, Social Login as well as Email & Password based login.

Application Properties

Spring Security 5 has in-built support for Google, Facebook, Github Social Login. We just need to configure the clientId, clientSecret and facebook.user-info-uri in the application.properties file.

Apart from this, we also need to configure provider, authorization-uri, token-uri, user-info-uri and user-name-attribute for LinkedIn Social Login.

application.properties

Replace all the <your-client-id> and <your-client-secret> with your app credentials from the respective social login provider

# Database configuration props
spring.datasource.url=jdbc:mysql://localhost:3306/demo?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Hibernate props
spring.jpa.show-sql=true
#spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

# Social login provider props
spring.security.oauth2.client.registration.google.clientId=<your-client-id>
spring.security.oauth2.client.registration.google.clientSecret=<your-client-secret>
spring.security.oauth2.client.registration.facebook.clientId=<your-client-id>
spring.security.oauth2.client.registration.facebook.clientSecret=<your-client-secret>
spring.security.oauth2.client.provider.facebook.user-info-uri=https://graph.facebook.com/me?fields=id,name,email,picture
spring.security.oauth2.client.registration.github.clientId=<your-client-id>
spring.security.oauth2.client.registration.github.clientSecret=<your-client-secret>
spring.security.oauth2.client.registration.linkedin.clientId=<your-client-id>
spring.security.oauth2.client.registration.linkedin.clientSecret=<your-client-secret>
spring.security.oauth2.client.registration.linkedin.client-authentication-method=post
spring.security.oauth2.client.registration.linkedin.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.linkedin.scope=r_liteprofile, r_emailaddress
spring.security.oauth2.client.registration.linkedin.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.linkedin.client-name=Linkedin
spring.security.oauth2.client.registration.linkedin.provider=linkedin
spring.security.oauth2.client.provider.linkedin.authorization-uri=https://www.linkedin.com/oauth/v2/authorization
spring.security.oauth2.client.provider.linkedin.token-uri=https://www.linkedin.com/oauth/v2/accessToken
spring.security.oauth2.client.provider.linkedin.user-info-uri=https://api.linkedin.com/v2/me
spring.security.oauth2.client.provider.linkedin.user-name-attribute=id
linkedin.email-address-uri=https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))

app.auth.tokenSecret=926D96C90030DD58429D2751AC1BDBBC
app.auth.tokenExpirationMsec=864000000
    # After successfully authenticating with the OAuth2 Provider,
    # we'll be generating an auth token for the user and sending the token to the
    # redirectUri mentioned by the frontend client in the /oauth2/authorization request.
    # We're not using cookies because they won't work well in mobile clients.
app.oauth2.authorizedRedirectUris=http://localhost:8081/oauth2/redirect,myandroidapp://oauth2/redirect,myiosapp://oauth2/redirect
# For detailed logging during development
#logging.level.com=TRACE
logging.level.org.springframework=TRACE
#logging.level.org.hibernate.SQL=TRACE
#logging.level.org.hibernate.type=TRACE

AppProperties.java

@ConfigurationProperties annotation binds all the configurations prefixed with app to the POJO

package com.javachinna.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
	private final Auth auth = new Auth();
	private final OAuth2 oauth2 = new OAuth2();

	public static class Auth {
		private String tokenSecret;
		private long tokenExpirationMsec;

		public String getTokenSecret() {
			return tokenSecret;
		}

		public void setTokenSecret(String tokenSecret) {
			this.tokenSecret = tokenSecret;
		}

		public long getTokenExpirationMsec() {
			return tokenExpirationMsec;
		}

		public void setTokenExpirationMsec(long tokenExpirationMsec) {
			this.tokenExpirationMsec = tokenExpirationMsec;
		}
	}

	public static final class OAuth2 {
		private List<String> authorizedRedirectUris = new ArrayList<>();

		public List<String> getAuthorizedRedirectUris() {
			return authorizedRedirectUris;
		}

		public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
			this.authorizedRedirectUris = authorizedRedirectUris;
			return this;
		}
	}

	public Auth getAuth() {
		return auth;
	}

	public OAuth2 getOauth2() {
		return oauth2;
	}
}

Web MVC Configuration

WebConfig.java

This configuration class enables CORS to allow our Angular client application access REST API’s from different origins. It allows all the origins which should be restricted to specific origins only in production.

It also configures the default locale, MessageSource for validation messages

package com.javachinna.config;

import java.util.Locale;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

@Configuration
public class WebConfig implements WebMvcConfigurer {

	private final long MAX_AGE_SECS = 3600;

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**").allowedOrigins("*").allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE").maxAge(MAX_AGE_SECS);
	}

	@Bean
	public MessageSource messageSource() {
		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
		messageSource.setBasename("classpath:messages");
		messageSource.setDefaultEncoding("UTF-8");
		return messageSource;
	}

	@Bean
	public LocaleResolver localeResolver() {
		final CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
		cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
		return cookieLocaleResolver;
	}

	@Override
	public Validator getValidator() {
		LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
		validator.setValidationMessageSource(messageSource());
		return validator;
	}
}

messages_en.properties

NotEmpty=This field is required.
Size.userDto.password=Try one with at least 6 characters.

Web Security Configuration

WebSecurityConfig.java

@EnableWebSecurity annotation indicates that this class is a Spring Security Configuration.

@EnableGlobalMethodSecurity provides AOP security on methods, some of the annotations it will enable are @PreAuthorize and @PostAuthorize

package com.javachinna.config;

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.client.RestTemplate;

import com.javachinna.security.jwt.TokenAuthenticationFilter;
import com.javachinna.security.oauth2.CustomOAuth2UserService;
import com.javachinna.security.oauth2.CustomOidcUserService;
import com.javachinna.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository;
import com.javachinna.security.oauth2.OAuth2AccessTokenResponseConverterWithDefaults;
import com.javachinna.security.oauth2.OAuth2AuthenticationFailureHandler;
import com.javachinna.security.oauth2.OAuth2AuthenticationSuccessHandler;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private CustomOAuth2UserService customOAuth2UserService;

	@Autowired
	CustomOidcUserService customOidcUserService;

	@Autowired
	private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

	@Autowired
	private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {

		http
			.cors()
				.and()
			.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
				.and()
			.csrf().disable()
			.formLogin().disable()
			.httpBasic().disable()
			.exceptionHandling()
				.authenticationEntryPoint(new RestAuthenticationEntryPoint())
				.and()
			.authorizeRequests()
				.antMatchers("/", "/error", "/api/all", "/api/auth/**", "/oauth2/**").permitAll()
			.anyRequest()
				.authenticated()
				.and()
			.oauth2Login()
				.authorizationEndpoint()
					.authorizationRequestRepository(cookieAuthorizationRequestRepository())
					.and()
				.redirectionEndpoint()
					.and()
				.userInfoEndpoint()
					.oidcUserService(customOidcUserService)
					.userService(customOAuth2UserService)
					.and()
				.tokenEndpoint()
					.accessTokenResponseClient(authorizationCodeTokenResponseClient())
					.and()
				.successHandler(oAuth2AuthenticationSuccessHandler)
				.failureHandler(oAuth2AuthenticationFailureHandler);

		// Add our custom Token based authentication filter
		http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}

	@Bean
	public TokenAuthenticationFilter tokenAuthenticationFilter() {
		return new TokenAuthenticationFilter();
	}

	/*
	 * By default, Spring OAuth2 uses
	 * HttpSessionOAuth2AuthorizationRequestRepository to save the authorization
	 * request. But, since our service is stateless, we can't save it in the
	 * session. We'll save the request in a Base64 encoded cookie instead.
	 */
	@Bean
	public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
		return new HttpCookieOAuth2AuthorizationRequestRepository();
	}

	// This bean is load the user specific data when form login is used.
	@Override
	public UserDetailsService userDetailsService() {
		return userDetailsService;
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder(10);
	}

	@Bean(BeanIds.AUTHENTICATION_MANAGER)
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
		OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
		tokenResponseHttpMessageConverter.setTokenResponseConverter(new OAuth2AccessTokenResponseConverterWithDefaults());
		RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
		restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
		DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
		tokenResponseClient.setRestOperations(restTemplate);
		return tokenResponseClient;
	}
}

RestAuthenticationEntryPoint.java

This class is responsible for sending HTTP status 401 Unauthorized response when a user tries to access a protected resource without authentication

package com.javachinna.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

	private static final Logger logger = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class);

	@Override
	public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
		logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
		httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getLocalizedMessage());
	}
}

JWT Authentication

Creating Token Provider

TokenProvider is responsible for token creation, validation, and getting user ID from the token. It makes use of the io.jsonwebtoken.Jwts for achieving this.

TokenProvider.java

package com.javachinna.security.jwt;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import com.javachinna.config.AppProperties;
import com.javachinna.dto.LocalUser;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

@Service
public class TokenProvider {

	private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

	private AppProperties appProperties;

	public TokenProvider(AppProperties appProperties) {
		this.appProperties = appProperties;
	}

	public String createToken(Authentication authentication) {
		LocalUser userPrincipal = (LocalUser) authentication.getPrincipal();

		Date now = new Date();
		Date expiryDate = new Date(now.getTime() + appProperties.getAuth().getTokenExpirationMsec());

		return Jwts.builder().setSubject(Long.toString(userPrincipal.getUser().getId())).setIssuedAt(new Date()).setExpiration(expiryDate)
				.signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret()).compact();
	}

	public Long getUserIdFromToken(String token) {
		Claims claims = Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(token).getBody();

		return Long.parseLong(claims.getSubject());
	}

	public boolean validateToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException ex) {
			logger.error("Invalid JWT signature");
		} catch (MalformedJwtException ex) {
			logger.error("Invalid JWT token");
		} catch (ExpiredJwtException ex) {
			logger.error("Expired JWT token");
		} catch (UnsupportedJwtException ex) {
			logger.error("Unsupported JWT token");
		} catch (IllegalArgumentException ex) {
			logger.error("JWT claims string is empty.");
		}
		return false;
	}
}

Creating Token Authentication Filter

The TokenAuthenticationFilter extends the Spring Web Filter OncePerRequestFilter class. For any incoming request, this Filter class gets executed. It checks if the request has a valid JWT token. If it has a valid JWT Token then it sets the Authentication in the context, to specify that the current user is authenticated.

TokenAuthenticationFilter.java

package com.javachinna.security.jwt;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.javachinna.service.LocalUserDetailService;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private TokenProvider tokenProvider;

	@Autowired
	private LocalUserDetailService customUserDetailsService;

	private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		try {
			String jwt = getJwtFromRequest(request);

			if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
				Long userId = tokenProvider.getUserIdFromToken(jwt);

				UserDetails userDetails = customUserDetailsService.loadUserById(userId);
				UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
				authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		} catch (Exception ex) {
			logger.error("Could not set user authentication in security context", ex);
		}

		filterChain.doFilter(request, response);
	}

	private String getJwtFromRequest(HttpServletRequest request) {
		String bearerToken = request.getHeader("Authorization");
		if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
			return bearerToken.substring(7, bearerToken.length());
		}
		return null;
	}
}

OAuth2 Authentication

Creating Custom OAuth2 Services

CustomOAuth2UserService.java

The CustomOAuth2UserService extends Spring Security’s DefaultOAuth2UserService and implements its loadUser() method. This method is called after an access token is obtained from the OAuth2 provider.

In this method, we first fetch the user’s details from the OAuth2 provider. If a user with the same email already exists in our database then we update his details, otherwise, we register a new user.

If the OAuth2 provider is LinkedIn, then we need to invoke the email address endpoint with the access token to get the user email address since it will not be returned in the response of user-info endpoint.

package com.javachinna.security.oauth2;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;

import com.javachinna.dto.SocialProvider;
import com.javachinna.exception.OAuth2AuthenticationProcessingException;
import com.javachinna.service.UserService;

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

	@Autowired
	private UserService userService;

	@Autowired
	private Environment env;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
		OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
		try {
			Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes());
			String provider = oAuth2UserRequest.getClientRegistration().getRegistrationId();
			if (provider.equals(SocialProvider.LINKEDIN.getProviderType())) {
				populateEmailAddressFromLinkedIn(oAuth2UserRequest, attributes);
			}
			return userService.processUserRegistration(provider, attributes, null, null);
		} catch (AuthenticationException ex) {
			throw ex;
		} catch (Exception ex) {
			ex.printStackTrace();
			// Throwing an instance of AuthenticationException will trigger the
			// OAuth2AuthenticationFailureHandler
			throw new OAuth2AuthenticationProcessingException(ex.getMessage(), ex.getCause());
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void populateEmailAddressFromLinkedIn(OAuth2UserRequest oAuth2UserRequest, Map<String, Object> attributes) throws OAuth2AuthenticationException {
		String emailEndpointUri = env.getProperty("linkedin.email-address-uri");
		Assert.notNull(emailEndpointUri, "LinkedIn email address end point required");
		RestTemplate restTemplate = new RestTemplate();
		HttpHeaders headers = new HttpHeaders();
		headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + oAuth2UserRequest.getAccessToken().getTokenValue());
		HttpEntity<?> entity = new HttpEntity<>("", headers);
		ResponseEntity<Map> response = restTemplate.exchange(emailEndpointUri, HttpMethod.GET, entity, Map.class);
		List<?> list = (List<?>) response.getBody().get("elements");
		Map map = (Map<?, ?>) ((Map<?, ?>) list.get(0)).get("handle~");
		attributes.putAll(map);
	}
}

CustomOidcUserService.java

OidcUserService is an implementation of an OAuth2UserService that supports OpenID Connect 1.0 Providers. Google is an OpenID Connect provider. Hence, we are creating this service to load the user with Google’s OAuth 2.0 APIs.

The CustomOidcUserService extends Spring Security’s OidcUserService and implements its loadUser() method. This method is called after an access token is obtained from the OAuth2 provider.

In this method, we first fetch the user’s details from the OAuth2 provider. If a user with the same email already exists in our database then we update his details, otherwise, we register a new user.

package com.javachinna.security.oauth2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

import com.javachinna.exception.OAuth2AuthenticationProcessingException;
import com.javachinna.service.UserService;

@Service
public class CustomOidcUserService extends OidcUserService {

	@Autowired
	private UserService userService;

	@Override
	public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
		OidcUser oidcUser = super.loadUser(userRequest);
		try {
			return userService.processUserRegistration(userRequest.getClientRegistration().getRegistrationId(), oidcUser.getAttributes(), oidcUser.getIdToken(),
					oidcUser.getUserInfo());
		} catch (AuthenticationException ex) {
			throw ex;
		} catch (Exception ex) {
			ex.printStackTrace();
			// Throwing an instance of AuthenticationException will trigger the
			// OAuth2AuthenticationFailureHandler
			throw new OAuth2AuthenticationProcessingException(ex.getMessage(), ex.getCause());
		}
	}
}

Creating Authorization Request Repository

The OAuth2 protocol recommends using a state parameter to prevent CSRF attacks. During authentication, the application sends this parameter in the authorization request, and the OAuth2 provider returns this parameter unchanged in the OAuth2 callback.

The application compares the value of the state parameter returned from the OAuth2 provider with the value that it had sent initially. If they don’t match then it denies the authentication request.

To achieve this flow, the application needs to store the state parameter somewhere so that it can later compare it with the state returned from the OAuth2 provider.

HttpCookieOAuth2AuthorizationRequestRepository.java

This class is responsible for storing and retrieving the OAuth2 authorization request and redirect_uri of the Angular client in the cookies.

package com.javachinna.security.oauth2;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;

import com.javachinna.util.CookieUtils;
import com.nimbusds.oauth2.sdk.util.StringUtils;

@Component
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
	public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
	public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
	private static final int cookieExpireSeconds = 180;

	@Override
	public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
		return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME).map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
				.orElse(null);
	}

	@Override
	public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
		if (authorizationRequest == null) {
			CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
			CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
			return;
		}

		CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);
		String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
		if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
			CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
		}
	}

	@Override
	public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
		return this.loadAuthorizationRequest(request);
	}

	public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
		CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
		CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
	}
}

Creating Custom OAuth2 Access Token Response Converter

OAuth2AccessTokenResponseConverterWithDefaults.java

LinkedIn OAuth2 access token API is returning only the access_token and expires_in values but not the token_type in the response. This results in the following error.

org.springframework.http.converter.HttpMessageNotReadableException: An error occurred reading the OAuth 2.0 Access Token Response: tokenType cannot be null; nested exception is java.lang.IllegalArgumentException: tokenType cannot be null

Hence the default OAuth2AccessTokenResponseConverter class has been copied as OAuth2AccessTokenResponseConverterWithDefaults and modified to set a default token type to resolve this issue

package com.javachinna.security.oauth2;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * @author Joe Grandja
 */
public class OAuth2AccessTokenResponseConverterWithDefaults implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {
	private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream
			.of(OAuth2ParameterNames.ACCESS_TOKEN, OAuth2ParameterNames.TOKEN_TYPE, OAuth2ParameterNames.EXPIRES_IN, OAuth2ParameterNames.REFRESH_TOKEN, OAuth2ParameterNames.SCOPE)
			.collect(Collectors.toSet());

	private OAuth2AccessToken.TokenType defaultAccessTokenType = OAuth2AccessToken.TokenType.BEARER;

	@Override
	public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
		String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

		OAuth2AccessToken.TokenType accessTokenType = this.defaultAccessTokenType;
		if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) {
			accessTokenType = OAuth2AccessToken.TokenType.BEARER;
		}

		long expiresIn = 0;
		if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) {
			try {
				expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
			} catch (NumberFormatException ex) {
			}
		}

		Set<String> scopes = Collections.emptySet();
		if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
			String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
			scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")).collect(Collectors.toSet());
		}

		Map<String, Object> additionalParameters = new LinkedHashMap<>();
		tokenResponseParameters.entrySet().stream().filter(e -> !TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey()))
				.forEach(e -> additionalParameters.put(e.getKey(), e.getValue()));

		return OAuth2AccessTokenResponse.withToken(accessToken).tokenType(accessTokenType).expiresIn(expiresIn).scopes(scopes).additionalParameters(additionalParameters).build();
	}

	public final void setDefaultAccessTokenType(OAuth2AccessToken.TokenType defaultAccessTokenType) {
		Assert.notNull(defaultAccessTokenType, "defaultAccessTokenType cannot be null");
		this.defaultAccessTokenType = defaultAccessTokenType;
	}
}

Creating Authentication Handlers

OAuth2AuthenticationSuccessHandler.java

On successful authentication, Spring security invokes the onAuthenticationSuccess() method of the OAuth2AuthenticationSuccessHandler configured in WebSecurityConfig.

This method,

  • Fetches the redirect_uri sent by the angular client from the cookie and validates against the list of allowed URI’s configured in the application.properties. if it is unauthorized redirect_uri, then it throws an exception
  • Creates a JWT authentication token
  • Redirects the user to the angular client redirect_uri with the JWT token added in the query string.
package com.javachinna.security.oauth2;

import static com.javachinna.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME;

import java.io.IOException;
import java.net.URI;
import java.util.Optional;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import com.javachinna.config.AppProperties;
import com.javachinna.exception.BadRequestException;
import com.javachinna.security.jwt.TokenProvider;
import com.javachinna.util.CookieUtils;

@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

	private TokenProvider tokenProvider;

	private AppProperties appProperties;

	private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

	@Autowired
	OAuth2AuthenticationSuccessHandler(TokenProvider tokenProvider, AppProperties appProperties,
			HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository) {
		this.tokenProvider = tokenProvider;
		this.appProperties = appProperties;
		this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository;
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		String targetUrl = determineTargetUrl(request, response, authentication);

		if (response.isCommitted()) {
			logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
			return;
		}

		clearAuthenticationAttributes(request, response);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}

	@Override
	protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);

		if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
			throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
		}

		String targetUrl = redirectUri.orElse(getDefaultTargetUrl());

		String token = tokenProvider.createToken(authentication);

		return UriComponentsBuilder.fromUriString(targetUrl).queryParam("token", token).build().toUriString();
	}

	protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
		super.clearAuthenticationAttributes(request);
		httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
	}

	private boolean isAuthorizedRedirectUri(String uri) {
		URI clientRedirectUri = URI.create(uri);

		return appProperties.getOauth2().getAuthorizedRedirectUris().stream().anyMatch(authorizedRedirectUri -> {
			// Only validate host and port. Let the clients use different paths if they want
			// to
			URI authorizedURI = URI.create(authorizedRedirectUri);
			if (authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) && authorizedURI.getPort() == clientRedirectUri.getPort()) {
				return true;
			}
			return false;
		});
	}
}

OAuth2AuthenticationFailureHandler.java

On Authentication failure, Spring Security invokes the onAuthenticationFailure() method of the OAuth2AuthenticationFailureHandler that we have configured in WebSecurityConfig.

This method,

  • Fetches the client redirect_uri from the cookie
  • Removes Authorization request cookie as well ass redirect_uri cookie
  • Redirects the user to the angular client with the error message added to the query string
package com.javachinna.security.oauth2;

import static com.javachinna.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import com.javachinna.util.CookieUtils;

@Component
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

	@Autowired
	HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue).orElse(("/"));

		targetUrl = UriComponentsBuilder.fromUriString(targetUrl).queryParam("error", exception.getLocalizedMessage()).build().toUriString();

		httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);

		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
}

Creating OAuth2 User Info Mappings

Every OAuth2 provider returns a different JSON response when we fetch the authenticated user’s details. Spring security parses the response in the form of a generic map of key-value pairs.

The following classes are used to get the required details of the user from the generic map of key-value pairs

OAuth2UserInfo.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

public abstract class OAuth2UserInfo {
	protected Map<String, Object> attributes;

	public OAuth2UserInfo(Map<String, Object> attributes) {
		this.attributes = attributes;
	}

	public Map<String, Object> getAttributes() {
		return attributes;
	}

	public abstract String getId();

	public abstract String getName();

	public abstract String getEmail();

	public abstract String getImageUrl();
}

FacebookOAuth2UserInfo.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

public class FacebookOAuth2UserInfo extends OAuth2UserInfo {
	public FacebookOAuth2UserInfo(Map<String, Object> attributes) {
		super(attributes);
	}

	@Override
	public String getId() {
		return (String) attributes.get("id");
	}

	@Override
	public String getName() {
		return (String) attributes.get("name");
	}

	@Override
	public String getEmail() {
		return (String) attributes.get("email");
	}

	@Override
	@SuppressWarnings("unchecked")
	public String getImageUrl() {
		if (attributes.containsKey("picture")) {
			Map<String, Object> pictureObj = (Map<String, Object>) attributes.get("picture");
			if (pictureObj.containsKey("data")) {
				Map<String, Object> dataObj = (Map<String, Object>) pictureObj.get("data");
				if (dataObj.containsKey("url")) {
					return (String) dataObj.get("url");
				}
			}
		}
		return null;
	}
}

GithubOAuth2UserInfo.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

public class GithubOAuth2UserInfo extends OAuth2UserInfo {

	public GithubOAuth2UserInfo(Map<String, Object> attributes) {
		super(attributes);
	}

	@Override
	public String getId() {
		return ((Integer) attributes.get("id")).toString();
	}

	@Override
	public String getName() {
		return (String) attributes.get("name");
	}

	@Override
	public String getEmail() {
		return (String) attributes.get("email");
	}

	@Override
	public String getImageUrl() {
		return (String) attributes.get("avatar_url");
	}
}

GoogleOAuth2UserInfo.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

public class GoogleOAuth2UserInfo extends OAuth2UserInfo {

	public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
		super(attributes);
	}

	@Override
	public String getId() {
		return (String) attributes.get("sub");
	}

	@Override
	public String getName() {
		return (String) attributes.get("name");
	}

	@Override
	public String getEmail() {
		return (String) attributes.get("email");
	}

	@Override
	public String getImageUrl() {
		return (String) attributes.get("picture");
	}
}

LinkedinOAuth2UserInfo.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

public class LinkedinOAuth2UserInfo extends OAuth2UserInfo {

	public LinkedinOAuth2UserInfo(Map<String, Object> attributes) {
		super(attributes);
	}

	@Override
	public String getId() {
		return (String) attributes.get("id");
	}

	@Override
	public String getName() {
		return ((String) attributes.get("localizedFirstName")).concat(" ").concat((String) attributes.get("localizedLastName"));
	}

	@Override
	public String getEmail() {
		return (String) attributes.get("emailAddress");
	}

	@Override
	public String getImageUrl() {
		return (String) attributes.get("pictureUrl");
	}
}

OAuth2UserInfoFactory.java

package com.javachinna.security.oauth2.user;

import java.util.Map;

import com.javachinna.dto.SocialProvider;
import com.javachinna.exception.OAuth2AuthenticationProcessingException;

public class OAuth2UserInfoFactory {
	public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
		if (registrationId.equalsIgnoreCase(SocialProvider.GOOGLE.getProviderType())) {
			return new GoogleOAuth2UserInfo(attributes);
		} else if (registrationId.equalsIgnoreCase(SocialProvider.FACEBOOK.getProviderType())) {
			return new FacebookOAuth2UserInfo(attributes);
		} else if (registrationId.equalsIgnoreCase(SocialProvider.GITHUB.getProviderType())) {
			return new GithubOAuth2UserInfo(attributes);
		} else if (registrationId.equalsIgnoreCase(SocialProvider.LINKEDIN.getProviderType())) {
			return new LinkedinOAuth2UserInfo(attributes);
		} else if (registrationId.equalsIgnoreCase(SocialProvider.TWITTER.getProviderType())) {
			return new GithubOAuth2UserInfo(attributes);
		} else {
			throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
		}
	}
}

Creating Controllers

AuthController.java

This controller exposes 2 POST API’s for User Login and Registration requests

package com.javachinna.controller;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.javachinna.dto.ApiResponse;
import com.javachinna.dto.JwtAuthenticationResponse;
import com.javachinna.dto.LocalUser;
import com.javachinna.dto.LoginRequest;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.security.jwt.TokenProvider;
import com.javachinna.service.UserService;
import com.javachinna.util.GeneralUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {

	@Autowired
	AuthenticationManager authenticationManager;

	@Autowired
	UserService userService;

	@Autowired
	TokenProvider tokenProvider;

	@PostMapping("/signin")
	public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
		Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
		SecurityContextHolder.getContext().setAuthentication(authentication);
		String jwt = tokenProvider.createToken(authentication);
		LocalUser localUser = (LocalUser) authentication.getPrincipal();
		return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, GeneralUtils.buildUserInfo(localUser)));
	}

	@PostMapping("/signup")
	public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
		try {
			userService.registerNewUser(signUpRequest);
		} catch (UserAlreadyExistAuthenticationException e) {
			log.error("Exception Ocurred", e);
			return new ResponseEntity<>(new ApiResponse(false, "Email Address already in use!"), HttpStatus.BAD_REQUEST);
		}
		return ResponseEntity.ok().body(new ApiResponse(true, "User registered successfully"));
	}
}

UserController.java

This controller exposes API’s for fetching public content as well as user role specific contents

package com.javachinna.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.javachinna.config.CurrentUser;
import com.javachinna.dto.LocalUser;
import com.javachinna.util.GeneralUtils;

@RestController
@RequestMapping("/api")
public class UserController {

	@GetMapping("/user/me")
	@PreAuthorize("hasRole('USER')")
	public ResponseEntity<?> getCurrentUser(@CurrentUser LocalUser user) {
		return ResponseEntity.ok(GeneralUtils.buildUserInfo(user));
	}

	@GetMapping("/all")
	public ResponseEntity<?> getContent() {
		return ResponseEntity.ok("Public content goes here");
	}

	@GetMapping("/user")
	@PreAuthorize("hasRole('USER')")
	public ResponseEntity<?> getUserContent() {
		return ResponseEntity.ok("User content goes here");
	}

	@GetMapping("/admin")
	@PreAuthorize("hasRole('ADMIN')")
	public ResponseEntity<?> getAdminContent() {
		return ResponseEntity.ok("Admin content goes here");
	}

	@GetMapping("/mod")
	@PreAuthorize("hasRole('MODERATOR')")
	public ResponseEntity<?> getModeratorContent() {
		return ResponseEntity.ok("Moderator content goes here");
	}
}

CurrentUser.java

CurrentUser is a meta-annotation that can be used to inject the currently authenticated user principal in the controllers

package com.javachinna.config;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

@Target({ ElementType.PARAMETER, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {

}

Creating Users / Roles on Application Startup

SetupDataLoader.java

package com.javachinna.config;

import java.util.Calendar;
import java.util.Date;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.dto.SocialProvider;
import com.javachinna.model.Role;
import com.javachinna.model.User;
import com.javachinna.repo.RoleRepository;
import com.javachinna.repo.UserRepository;

@Component
public class SetupDataLoader implements ApplicationListener<ContextRefreshedEvent> {

	private boolean alreadySetup = false;

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private RoleRepository roleRepository;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Override
	@Transactional
	public void onApplicationEvent(final ContextRefreshedEvent event) {
		if (alreadySetup) {
			return;
		}
		// Create initial roles
		Role userRole = createRoleIfNotFound(Role.ROLE_USER);
		Role adminRole = createRoleIfNotFound(Role.ROLE_ADMIN);
		Role modRole = createRoleIfNotFound(Role.ROLE_MODERATOR);
		createUserIfNotFound("[email protected]", Set.of(userRole, adminRole, modRole));
		alreadySetup = true;
	}

	@Transactional
	private final User createUserIfNotFound(final String email, Set<Role> roles) {
		User user = userRepository.findByEmail(email);
		if (user == null) {
			user = new User();
			user.setDisplayName("Admin");
			user.setEmail(email);
			user.setPassword(passwordEncoder.encode("admin@"));
			user.setRoles(roles);
			user.setProvider(SocialProvider.LOCAL.getProviderType());
			user.setEnabled(true);
			Date now = Calendar.getInstance().getTime();
			user.setCreatedDate(now);
			user.setModifiedDate(now);
			user = userRepository.save(user);
		}
		return user;
	}

	@Transactional
	private final Role createRoleIfNotFound(final String name) {
		Role role = roleRepository.findByName(name);
		if (role == null) {
			role = roleRepository.save(new Role(name));
		}
		return role;
	}
}

Creating Spring Boot Main Application Class

DemoApplication.java

package com.javachinna;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication(scanBasePackages = "com.javachinna")
@EnableJpaRepositories
@EnableTransactionManagement
public class DemoApplication extends SpringBootServletInitializer {

	public static void main(String[] args) {
		SpringApplicationBuilder app = new SpringApplicationBuilder(DemoApplication.class);
		app.run();
	}

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(DemoApplication.class);
	}
}

Run with Maven

You can run the application using mvn clean spring-boot:run and the REST API services can be accessed via http://localhost:8080

What’s next?

In this article, we have configured Spring Security OAuth2 Social Login, JWT authentication. In the next article, we will create the angular client application to consume this REST API.

This Post Has 56 Comments

  1. Vishnu

    How to Build Spring Boot Angular User Registration and OAuth2 Social Login with Facebook, Google, LinkedIn, and Github – Part 1 and part 2 send me git hub link foe download Please..

  2. Vishnu

    Very Nice example.
    Thanks
    Send me source code of part 1 and part 2 ya send me git hub link Spring Boot Angular User Registration and OAuth2 Social Login with Facebook, Google, LinkedIn, and Github

    1. Chinna

      You cand find the complete source code github link at the end of Part 3

  3. Vishnu

    SignUpRequest.java and User.java in this POJO class not created setter and getter showing errors in demo project.
    Can you tell me create setter and getter need.

    1. Chinna

      I have used Lombok library for generating the boilerplate code in this demo application. So you need to install the Lombok plugin in your IDE. You can follow this guide https://www.baeldung.com/lombok-ide for more info.

      1. Vishnu

        Thanks…

  4. Vishnu

    I have some error comes when i have run application.
    Failed to load org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer
    Caused by: java.lang.ClassNotFoundException: liquibase.exception.ChangeLogParseException
    Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name
    ‘webSecurityConfig’: Unsatisfied dependency expressed through method ‘setContentNegotationStrategy’ parameter 0;
    Error creating bean with name ‘org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration’: Unsatisfied dependency expressed through method ‘setConfigurers’ parameter 0;
    Error creating bean with name ‘org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration’: Unsatisfied dependency expressed through method ‘setConfigurers’ parameter 0;
    Error creating bean with name ‘entityManagerFactory’ defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory;
    Unable to open JDBC Connection for DDL execution
    Access denied for user ‘root’@’localhost’ (using password: YES)
    My mysql version is 5.7.32

    1. Chinna

      Check your database credentials in the application.properties file.

  5. Vishnu

    How To Build Angular With Java Backend For Production this application

  6. Vishnu

    I have put angular prod build files into resources/static folder and run spring application but error comes. Please check

    http://localhost:8080/home

    Whitelabel Error Page
    This application has no explicit mapping for /error, so you are seeing this as a fallback.

    Mon Dec 28 16:32:36 IST 2020
    There was an unexpected error (type=Unauthorized, status=401).
    Full authentication is required to access this resource

  7. Sam

    Hello Chinna – Great article. Do you know if there is a way to test the social login feature with localhost?

    I am getting authorization_request_not_found error which could be because the Session Cookie is being overwritten. Since you’re running the Authorization Server on http://localhost:8081 and the Client App on http://localhost:8080, the host names are the same so the Cookie from http://localhost:8080 is being overwritten with the Cookie assigned from http://localhost:8081. Any ideas how you got it to work locally?

    1. Chinna

      Hello Sam,

      There is no separate cookie for the client and server. Only the backend application will store the OAUTH2_AUTHORIZATION_REQUEST in the cookie. So there will not be any overwritting of the cookies. I have tested this application in localhost only.

  8. PhilB

    Hi Chinna, thanks for the good post.

    I’ve modified this to also generate a refresh token. However should this refresh token also be sent with the redirect URL?

    OR should there be an access code (short lived) which is then used to get both tokens?

    1. Chinna

      Hi Philip,

      Yes. The refresh token also should be sent in the redirect URL. So that it can be stored in the browser session. Later, when the access token (short-lived) is expired, this refresh token can be used to get a new access token. I think, if the refresh token also expired, then the application should logout. So that the user can login again and get a new access token and refresh token.

      Currently, this sample application uses a long-lived access token. It will automatically logout the user if the access token is expired. I assume that you will change this logic to logout when the refresh token is expired.

      I hope it helps.

      Cheers,
      Chinna

  9. Maroun

    Hi Chinna,

    Thank you so much for this great tutorial, and for the efforts you put into it.

    What’s the reason for executing “removeAuthorizationRequestCookies” in the on success handler?

    1. Maroun

      For some reason, I’m running into an infinite loop and requested to “reauthorize” by GitHub, until I finally got blocked due to “abuse detection” mechanism.

      1. Chinna

        That’s strange. I have never faced this issue. Did you enable Spring debug log and see what Github is returning in the response for the OAuth2 Authorization request? Do you see any errors in the log ?

        1. Maroun

          Thanks for your fast reply. I pasted the log here: https://justpaste.it/3k8ws

          It’d be great if you can assist with spotting the problem.

          1. Chinna

            I had a look at the log and I don’t find any response from github after the following authorization request.

            2021-01-21 10:35:25.513 DEBUG 63738 — [nio-8080-exec-8] o.s.s.web.DefaultRedirectStrategy : Redirecting to ‘https://github.com/login/oauth/authorize?

            Also, I could see this error in the log. It should not happen. But I’m not sure why it is happening

            I2021-01-21 10:35:39.179 DEBUG 63738 — [nio-8080-exec-9] .s.o.c.w.OAuth2LoginAuthenticationFilter : Authentication request failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found]

            org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found] (coming from filterChain.doFilter(request, response)).

        2. Maroun

          Hi Chinna, I just wanted to update you that it’s working now. The problem happens when I access “localhost” instead of “127.0.0.1” (since that’s what’s configured on both my Angular app and redirect URI in the auth app). Thanks for your help.

          1. Chinna

            Glad to know that it worked. Thanks for letting me know.

    2. Chinna

      Hi Maroun,

      OAuth2AuthorizationRequest and redirect_uri (angular client URI) will be stored in the cookie before redirecting to the OAuth2 Authorization server endpoint. Once authorization is successful, these are not required anymore. So they are removed from the cookies by calling “removeAuthorizationRequestCookies” method in the success handler.

  10. Chandu

    I am not able run my application. i am getting below error
    Responding with unauthorized error. Message – Full authentication is required to access this resource

    My Angular application is running on http://localhost:4200
    Spring boot application : http://localhost:8080
    May i know why we are redirecting to this url –> REDIRECT_URL = “?redirect_uri=http://localhost:8081/login”;

    1. Chinna

      redirect_uri is the angular client URL. Once the social login is successful, the spring backend application will use this URL to redirect back to the Angular application which initiated this social login process.

      Looks like you are using the default port number 4200 for the client application. This sample application uses 8081 port for the client application and 8080 for the backend. If you want to stick to the default port 4200, then you need to update the client URLs defined in both the front end and backend.

  11. Ilya

    Hi Chinna,

    That is the best example i’ve ever seen.

    Can you please help me to implement the code where I have to use Oauth2, JWT + LDAP authentication

    1. Chinna

      Hi Ilya,

      Thank you. Right now I’m pretty much occupied with other tasks. However, I’ll try to write an article on this combination when I get some time.

  12. Mat

    Hi,
    Great article. If I may, I would like to ask a question regarding OAuth2AuthenticationSuccessHandler class. My URI which for Angular would be http://localhost:4200/login is considered as an “Unauthorized Redirect URI”. I thought that here:

    Optional redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);

    if (redirectUri.isPresent()){
    throw new UnauthorizedUserException(“Sorry! We’ve got an Unauthorized Redirect URI and can’t proceed with the authentication”);
    }

    presence of a redirect URI in a cookie is a desirable action? Could you please explain that?

    1. Chinna

      Hi Mat,

      Since the Spring REST API is stateless, We are storing the redirect URI in the cookie and it will be removed once the authorization is complete.

      1. Mat

        Thanks for the reply.

        I understand the necessity of cookie usage, but I can’t quite figure out this code part itself:
        ________________________________________________________________________

        Optional redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);

        if (redirectUri.isPresent()){
        throw new UnauthorizedUserException(“Sorry! We’ve got an Unauthorized Redirect URI and can’t proceed with the authentication”);
        }

        String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
        ________________________________________________________________________
        Optional is fetching redirect URI that is stored in REDIRECT_URI_PARAM_COOKIE_NAME (in my case it was http://localhost:4200/login) and then there is and “if” statement that throws an error whenever this redirect_uri is present. After that there is another line, that uses fetched redirect_uri. In short, for me it sounds like : “search for uri, if present throw error, but later use it for targetUrl creation”. When we hope to receive the uri from cookie, why to throw an errow when it is present?
        I am really bothered because your tutorial is amazing, but it is the only part that I cannot understand properly.

  13. Chinna

    Hi Mat,

    This is the actual code where it throws error only if the redirect Uri is present in the cookie and it is not configured as an authorised URI in the application.properties file. It looks like You have removed the second condition since it was causing an issue for you.

    if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
    throw new BadRequestException(“Sorry! We’ve got an Unauthorized Redirect URI and can’t proceed with the authentication”);
    }

  14. Ilya

    Hey,

    In this code, don’t you have to verify if the user exists in DB ?

    @PostMapping(“/signin”)
    public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
    Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
    SecurityContextHolder.getContext().setAuthentication(authentication);
    String jwt = tokenProvider.createToken(authentication);
    LocalUser localUser = (LocalUser) authentication.getPrincipal();
    return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, GeneralUtils.buildUserInfo(localUser)));
    }

    1. Chinna

      The authenticationManager.authenticate() method will throw exception if the user doesn’t exist in the database. So no need to verify it explicitly.

  15. derteuffel

    ***************************
    APPLICATION FAILED TO START
    ***************************

    Description:

    Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ that could not be found.

    Action:

    Consider defining a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ in your configuration.

    Process finished with exit code 0

    i get this error can you please explain me what’s going on, and how can i resolve them? thank you.

    1. Chinna

      ClientRegistrationRepository class is part of spring-security-oauth2-client-5.4.1.jar. Check if this jar is present under the Maven Dependencies in your project. if not, Check if spring-boot-starter-oauth2-client dependency is present in your pom.xml. Add it if it is not there already.

      1. Priya

        I am getting the same error , even in pom xml file this dependency is already present.

        Could you please help me to resolve this issue?

  16. Ridma

    Great Article. Is there any document to refer for forgot password and email verification related to this article. If so please share it.

    1. Chinna

      I don’t have any document for implementing forgot password and email verification as of now. But I do have a Spring MVC application with these functionalities. Not sure if you are interested in that.

  17. P

    Hello Chinna
    Is this possible to create application that allows Angular to log in to spring boot with oauth2 social login but not utilize JWT? I mean keeping it withing session and use cookie to authenticate frontend within api.

    Thanjs

    1. Chinna

      If you don’t want to use JWT, then you can go for basic authentication. I think, you need to generate a random password for the users who logged in with Social Login. Currently, after successful social login authentication, the REST API returns both the JWT token and user details. Instead of the token, you need to return the password also. Then you can store the user info with password on the client. Then you can use basic auth on the backend and pass user name and password in the REST API calls from client to validate the user identity. But this is very less secure compared to JWT.

  18. mlodszy99

    I cant login with gmail and fb . Can you help me ? i want add
    new functionality of verifying the account by email to your code

    1. Chinna

      Sure. I can help you with the issues. Also, I’m planning to write a blog post on the email verification functionality shortly. It is in my to-do list. For now, you can refer to this article for sending emails on the back end.

  19. Harikrishna

    Hi Chinna, Thanks for the tutorial it was great one learnt a lot through your tutorial.
    Can you please share the Rest endpoints with JSON request and Header details to register user through postman if possible?

  20. Mehdi

    Hey Chinna
    I see that you have listed twitter as a provider, but there is no implementation of it like the others, any idea of how to add it too ?

    1. Chinna

      Many endpoints on the Twitter developer platform use the OAuth 1.0a method to act, or make API requests, on behalf of a Twitter account. But we are using OAuth2 for social login implementation. So, I didn’t implement Twitter login.

  21. Eze

    Thanks for the tutorial. Please I m getting this error ‘default message [Passwords don’t match]’ registering a new user. Please, How can I solve the problem. Thanks.

    1. Chinna

      This means passwords entered in Password and Confirm Password fields do not match. Are you getting this error even after entering the same password ?

  22. Samuel

    Hummm, your Social Login User seems to skip Two_Factor Authentication. What if 2FA is enabled for the User?

    1. Chinna

      Yes. Currently, 2FA is not supported for Social users. If you enable it for Social users, then you should generate and show the QR code to the user in order to get a TOTP from an Authenticator app.

  23. anonymous

    can you please share the parameters to test the signup endpoint

  24. Priya

    Hello, thank you for sharing this tutorial. I followed the tutorial for registration process, but in config file, getting error – for creating setupDataLoader beans, hence getting error for other beans creation as well. Could you please help me to resolve this issue?

    1. Chinna

      Hi, What errror are you getting? Can you send the stack trace? So that I can take a look at it.

  25. Priya

    As WebSecurityConfigurerAdapter deprecated, I implemented SecurityFilterChain, but facing many issues.
    Currently below error is showing up.

    Parameter 0 of method setFilterChains in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ that could not be found.

    Action:

    Consider defining a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ in your configuration.

  26. Shital

    Hi Sir,

    Thank you for providing this tutorial.
    I have followed the steps and used Security Filter chain as WebSecurityConfigurerAdapter deprecated.
    But getting below error. It would be great if you will help me on it. I tried to resolve this issue. This dependancy already added in pom file. still bean is not getting created.

    APPLICATION FAILED TO START
    ***************************

    Description:

    Parameter 0 of method setFilterChains in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ that could not be found.

    Action:

    Consider defining a bean of type ‘org.springframework.security.oauth2.client.registration.ClientRegistrationRepository’ in your configuration.

    1. Chinna

      Hi, there could be an issue with your application.properties file. Can you check if it has all the social login provider properties configured? for example,
      # Social login provider props
      spring.security.oauth2.client.registration.google.clientId=
      spring.security.oauth2.client.registration.google.clientSecret=

      And if you are using a spring profile, please make sure you are running the application with the right profile.

  27. OscarD

    Hello Chinna, your tuto is wonderfull but there are a lot of bug with spring-boot 3.1.0, java 17 and SecurityFilterChain.
    Bugs:
    – circular error between WebSecurityConfig class and customOAuth2UserService -> UserServiceImpl classes
    – The configuration syntaxe WebSecurityConfig has totally change in SecurityFilterChain
    Can you propose an updated version of your tuto ?

  28. Amrit

    Hi there, I am getting this error when i try to login through microsoft azure
    Login failed: [invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 401 Unauthorized: [no body]

Leave a Reply