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 theapplication.properties
. if it is unauthorizedredirect_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.
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..
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
You cand find the complete source code github link at the end of Part 3
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.
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.
Thanks…
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
Check your database credentials in the application.properties file.
How To Build Angular With Java Backend For Production this application
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
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?
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.
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?
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
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?
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.
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 ?
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.
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)).
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.
Glad to know that it worked. Thanks for letting me know.
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.
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”;
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.
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
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.
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?
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.
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.
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”);
}
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)));
}
The authenticationManager.authenticate() method will throw exception if the user doesn’t exist in the database. So no need to verify it explicitly.
***************************
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.
ClientRegistrationRepository
class is part ofspring-security-oauth2-client-5.4.1.jar
. Check if this jar is present under the Maven Dependencies in your project. if not, Check ifspring-boot-starter-oauth2-client
dependency is present in yourpom.xml
. Add it if it is not there already.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?
Great Article. Is there any document to refer for forgot password and email verification related to this article. If so please share it.
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.
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
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.
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
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.
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?
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 ?
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.
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.
This means passwords entered in Password and Confirm Password fields do not match. Are you getting this error even after entering the same password ?
Hummm, your Social Login User seems to skip Two_Factor Authentication. What if 2FA is enabled for the User?
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.
can you please share the parameters to test the signup endpoint
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?
Hi, What errror are you getting? Can you send the stack trace? So that I can take a look at it.
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.
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.
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.
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 ?
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]