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("admin@javachinna.com", 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 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.