Spring Security OAuth2 User Registration

In my last article, we developed a spring security 5 OAuth application with google sign in and customized most of the default behavior. In this article, we will take a deeper look into customizing OAuth2 login. We have already added social login support to our app and now we will extend it to have an option for custom user registration or signup using email and password. After successful registration, we should be able to support JWT token-based authentication in the app.

Primarily, we will be adding below support in our app.

  • Add custom login page in oauth2Login() element.

  • User can choose login options with either custom email and password or social login with Google OAuth.

  • After a successful login, JWT token should be generated and token-based authentication is enabled and user is redirected to /home.

Spring Security OAuth Configuration

To get started with the app, first of all let us review the OAuth configuration that we did in our last article. Below was the final security config where we have customized the oauth2Login() element to have custom redirection point, user info endpoint, user service, authorization endpoint etc. You can visit this article for details. SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private OidcUserService oidcUserService;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
            .oauth2Login()
                .redirectionEndpoint()
                .baseUri("/oauth2/callback/*")
            .and()
                .userInfoEndpoint()
                .oidcUserService(oidcUserService)
            .and()
                .authorizationEndpoint()
                .baseUri("/oauth2/authorize")
                .authorizationRequestRepository(customAuthorizationRequestRepository())
            .and()
                .successHandler(customAuthenticationSuccessHandler);

    }

    @Bean
    public AuthorizationRequestRepository customAuthorizationRequestRepository() {
        return new HttpSessionOAuth2AuthorizationRequestRepository();
    }


}

Customizing Login Endpoint

By default, the OAuth 2.0 Login Page is auto-generated by the DefaultLoginPageGeneratingFilter and it is available at /login. To override the default login page, configure oauth2Login().loginPage() with your custom url. Here, we hav configured it as /auth/custom-login. So, our configure() method becomes

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.loginPage("/auth/custom-login")
				...
				...
				.and()
                .successHandler(customAuthenticationSuccessHandler);
	}

Creating Custom Login Page

With the above configuration, whenever any authentication is required, user will be redirected to /auth/custom-login. Now let us create a login page as login.html. In the above security config, we have configured authorizationEndpoint as /oauth2/authorize and hence on the click of Google icon, user will be redirected to /oauth2/authorize/google login.html

<body>
<div class="container">
    <h1>SignUp !</h1>
    <div class="col-md-6">
        <div class="row">
                <p><a href="/oauth2/authorize/google"><img src="http://pngimg.com/uploads/google/google_PNG19635.png" style="height:60px"></a></p>
        </div>
        <div class="row">
            <h4>OR</h4>
        <div class="row">
            <form action="/auth/signup/" method="post" name="signup">
                <div class="form-group">
                    <label for="name">Name</label>
                    <input type="string" class="form-control" id="name" name="name">
                </div>
                <div class="form-group">
                    <label for="email">Email address:</label>
                    <input type="email" class="form-control" id="email" name="email">
                </div>
                <div class="form-group">
                    <label for="pwd">Password:</label>
                    <input type="password" class="form-control" id="pwd" name="password">
                </div>
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
                <button type="submit" class="btn btn-primary">Sign Up</button>
            </form>
        </div>
    </div>
    </div>
</div>
</body>

Spring Security Config for Registration

To configure our registration process we will be using exisitng implementation - Spring Boot Jwt. Below is our final security config to accomodate the custom login.

Here, we have injected UserDetailsService required by auth manager to fetch the users from the DB. We have configured our Bcrypt password encoder and added custom filter to intercept before UsernamePasswordAuthenticationFilter and validate the token and set the security context.

We have allowed all the request for matcher /auth and configured login page to be available at "/auth/custom-login". We have registred our custom authentication entry point that will redirect any unauthenticated user to login page. SecurityConfig .java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter implements ApplicationContextAware {

    @Autowired
    private OidcUserService oidcUserService;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

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

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

    @Bean
    public JwtAuthenticationFilter authenticationTokenFilterBean() {
        return new JwtAuthenticationFilter();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().
                antMatchers("/auth/**", "/webjars/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .oauth2Login()
                .loginPage("/auth/custom-login")
                .redirectionEndpoint()
                .baseUri("/oauth2/callback/*")
                .and()
                .userInfoEndpoint()
                .oidcUserService(oidcUserService)
                .and()
                .authorizationEndpoint()
                .baseUri("/oauth2/authorize")
                .authorizationRequestRepository(customAuthorizationRequestRepository())
                .and()
                .successHandler(customAuthenticationSuccessHandler);

        http
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    }

    @Bean
    public AuthorizationRequestRepository customAuthorizationRequestRepository() {
        return new HttpSessionOAuth2AuthorizationRequestRepository();
    }

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

}

With above configuration, we have achieved below points

1. If an user tries to access any secured page without login e.g. localhost:8080, then he will be redirected to localhost:8080/auth/custom-login.

2. On login page, we have 2 different options to login. Either user can signup with email address and password or else can choose to sign with Google.

3. In both the cases, after a successfull authentication the user will be redirected to /home. JwtAuthenticationEntryPoint.java

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        response.sendRedirect("/auth/custom-login");
    }
}

JWT Authentication Filter Implementation

Below is the filter implementation that intercepts all the request and checks if the token is present in the URL. If the token is present then it will validate the token and set the security context will be set and the request will be chained to next filter in the filter chain. This is the filter which will be executed before UsernamePasswordAuthenticationFilter. JwtAuthenticationFilter.java

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String token = req.getParameter(TOKEN_PARAM);
        String username = null;
        if (token != null) {
            try {
                username = jwtTokenUtil.getUsernameFromToken(token);
            } catch (IllegalArgumentException e) {
                logger.error("an error occured during getting username from token", e);
            } catch (ExpiredJwtException e) {
                logger.warn("the token is expired and not valid anymore", e);
            } catch(SignatureException e){
                logger.error("Authentication Failed. Username or Password not valid.");
            }
        } else {
            logger.warn("couldn't find bearer string, will ignore the header");
        }
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                logger.info("authenticated user " + username + ", setting security context");
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(req, res);
    }
}

Spring Controller for Custom Login

Below REST endpoints are responsible for generating our custom login page and accepts sign up request. AuthController.java

@Controller
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private UserService userService;

    @GetMapping("/custom-login")
    public String loadLoginPage(){
        return "login";
    }

    @PostMapping("/signup")
    public String login(@ModelAttribute("signup") User user){
        String token = userService.signUp(user);
        return UriComponentsBuilder.fromUriString(homeUrl)
                .queryParam("auth_token", token)
                .build().toUriString();
    }


}

UserController.java

This endpoint will be executed post login and loads the home page.

@Controller
public class UserController {

    @GetMapping("/home")
    public  String home(){
        return  "home";
    }
}

Service Implementation For Custom User Authentication

Below implementation will be used by spring security to authenticate user. UserDetailServiceImpl.java

@Service(value = "userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user == null){
            throw new UsernameNotFoundException("Invalid username or password.");
        }
        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), getAuthority());
    }

    private List getAuthority() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
}

Below implementation is for creating entries of new user in the DB. UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bcryptEncoder;

    @Override
    public String signUp(User user) {
        User dbUser = userRepository.findByEmail(user.getEmail());
        if (dbUser != null) {
            throw new RuntimeException("User already exist.");
        }
        user.setPassword(bcryptEncoder.encode(user.getPassword()));
        userRepository.save(user);
        return jwtTokenUtil.generateToken(user);
    }
}

Below is the util class that we are using for generating and validating our JWT token. JwtTokenUtil.java

@Component
public class JwtTokenUtil implements Serializable {

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public  T getClaimFromToken(String token, Function claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SIGNING_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(User user) {
        return doGenerateToken(user.getUsername());
    }

    private String doGenerateToken(String subject) {

        Claims claims = Jwts.claims().setSubject(subject);
        claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));

        return Jwts.builder()
                .setClaims(claims)
                .setIssuer("http://devglan.com")
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
                .signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (
              username.equals(userDetails.getUsername())
                    && !isTokenExpired(token));
    }

}

Running the Final Application

Import the project as a Maven project and make sure your DB configuration matches with those defined in application.properties.

Run SpringBootGoogleOauthApplication.java as a java application.

Now open your browser and hit localhost:8080/auth/custom-login to see the login page.

Conclusion

In this tutorial, we looked into providing support for custom user registration in an existing spring boot OAuth2 application. We used spring security 5 and JWT for our custom token generation process.

Last updated