skip to content

Simple Banking API without a DB II

/ 5 min read

Introduction

This post is a sequel of part 1 were talked about some design decisions, especially about building without a mainstream DB. This post is primarily on how I spent half of the time developing this app on spring-security 😫🤕. Nonetheless, this gave me a new picture of how spring applications work…If it hadn’t been for this aspect of the application, I might not have learnt about filters and servlets.

Entry Point…

Immediately after installing spring-security, all your endpoints will be inaccessible, and to access them, you must log in using a user generated by spring-security (spring security has some boilerplate configurations with a form login). Therefore, to personalize the process you create a class that extends WebSecurityConfigurerAdapter- is the entry point/file of spring security. The implementation is shown below.

@Configuration
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccountDetailsService myAccountDetailsService;
@Autowired
private JWTFilter jwtFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myAccountDetailsService).passwordEncoder(encoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests().antMatchers("/api/v1/login").permitAll()
.antMatchers("/api/v1/create_account").permitAll()
.anyRequest().authenticated()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}

We use the @Configuration to make this class a configuration file that would be used by spring. We have to extend the already existing configuration to add our personalized code that fits our application.

Using @Autowired we import from the spring container

  • MyAccountDetailsService - A customized service to access users
  • JWTFilter - A filter checking every request to resolve and validate JWT tokens

Using @Bean we are creating a bean - an object that is instantiated, assembled, and otherwise managed by a Spring IoC container

  • authenticationManagerBean() returns a manager that can be used to login and …
  • encoder() returns the default PasswordEncoder for the application in this case BCryptPasswordEncoder()

Implementing the WebSecurityConfigurerAdapter we have to override the configure() function. In this case, we are using method overloading to configure two different aspects of the security system

  1. protected void configure(AuthenticationManagerBuilder auth); In this function, we are setting the MyAccountDetailsService as the default service for user access and also set the default password encoder

  2. protected void configure(HttpSecurity http); Here we are configuring the http.

  • First, we set cors then we disable csrf since we are using JSON requests not forms
  • Allowed access to /api/v1/create_account and /api/v1/login without authentication.
  • We then define the session manager to be STATELESS
  • we then finally add the jwtFilter

Below is the implementation of the JWTFilter

@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired
JWTUtils jwtUtils;
@Autowired
private MyAccountDetailsService myAccountDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain)
throws ServletException, IOException {
String token = jwtUtils.resolveToken(req);
if(token != null && jwtUtils.validateToken(token)) {
UserDetails userDetails = myAccountDetailsService.loadUserByUsername(jwtUtils.getAccountName(token));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, "", new ArrayList<>());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
filterChain.doFilter(req, res);
}
}

In summary, we are resolving the token from the header and if it is not null, we validate the token. Let us just take the jwtUtils as a black box. Although, you can see full implementation here. The validateToken function throws an exception which we have to take care of as we progress.

Once the token is valid then we try to verify the user using myAccountDetailsService then set the security context with the new usernamePasswordAuthenticationToken. we then move on to the next filter using filterChain.doFilter(req, res).

Below is the implementation of myAccountDetailsService.

@Service
public class MyAccountDetailsService implements UserDetailsService {
@Autowired
AccountService accountService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
Account account = accountService.getAccount(username);
return User.builder().username(account.getAccountNumber()).password(account.getPassword()).authorities(new ArrayList<>()).build();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

To implement the UserDetaisService, we have to override the loadUserByUsername where we tell spring how to find our user.

Login

The AccountService has a simple login function see below

public String loginAccount(LoginRequest request) throws IOException {
try{
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getAccountNumber(),request.getAccountPassword()));
}catch (BadCredentialsException ex){
throw new ResponseStatusException(UNAUTHORIZED,ex.getMessage());
}
Account account = getAccount(request.getAccountNumber());
Map<String, String> claims = new HashMap<>(){{
put("accountName",account.getAccountName());
put("enabled","true");
}};
String token = jwtUtils.createToken(account.getAccountNumber(), claims);
return token;
}

The authenticationManager is autowired into the service, and a token is returned after a successful login.

Error Management

At this stage, everything was looking okay. However, spring-security was returning a generic error for every exception even after adding an exceptionHandler to the HttpSecurity configuration.

At the filter level, @ControllerAdvice cant manage the errors.

Filters happens before controllers are even resolved so exceptions thrown from filters can’t be caught by a Controller Advice.
site

To solve this issue, I built an exception handler filter to manage all errors in the filters.

@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (ResponseStatusException e) {
CustomResponse errorResponse = new CustomResponse(e.getStatus().value(),false,e.getMessage());
response.setContentType("application/json");
response.setStatus(e.getStatus().value());
response.getWriter().write(convertObjectToJson(errorResponse));
}
catch (RuntimeException e) {
CustomResponse errorResponse = new CustomResponse(500,false, e.getMessage());
response.setContentType("application/json");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(convertObjectToJson(errorResponse));
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}

This handler is to be added to the webSecurity configuration. The final implementation looks like

@Configuration
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccountDetailsService myAccountDetailsService;
@Autowired
private JWTFilter jwtFilter;
@Autowired
private ExceptionHandlerFilter exceptionHandlerFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myAccountDetailsService).passwordEncoder(encoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests().antMatchers("/api/v1/login").permitAll()
.antMatchers("/api/v1/create_account").permitAll()
.anyRequest().authenticated()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(exceptionHandlerFilter, CorsFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}

Line 26 autowires the ExceptionHandlerFilter while line 26 adds the filter before the CorsFIlter

Conclusion

It has been a long one, I pasted the snippets of the notable parts of the application in the post. Moreover, you can find the full implementation is here.