REST API with Java — Part 4

Spring Security

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. It is the standard for securing Spring based applications.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>

New Domain Classes

We will need to create a class to represent the application user, as well its roles:

@Data  @NoArgsConstructor   @AllArgsConstructor
@Entity @Table(name = "USERS")
public class AppUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private List<Role> roles = new ArrayList<>();
}
@Data @NoArgsConstructor @AllArgsConstructor
@Entity @Table(name = "ROLES")
public class Role {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;

public Role(String name) {
this.name = name;
}
}

New Controller, Services and Repositories

To handle operations for application Users, we will need to create classes in each layer to persist and provide data about Users and Roles.

@Repository
public interface UserRepo extends JpaRepository<AppUser, Integer> {
AppUser findByUsername(String username);
}
@Repository
public interface RoleRepo
extends JpaRepository<Role, Integer> {
Role findByName(String name);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Service 
@AllArgsConstructor @Transactional
public class UserService implements UserDetailsService {
private UserRepo userRepo;
private RoleRepo roleRepo;
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepo.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException("User not found");
Collection<SimpleGrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName())).collect(toList());
return new User(user.getUsername(), user.getPassword(), authorities);
}
// method implementations below omitted ... public AppUser save(AppUser user) { ... }

public AppUser find(String username) { ... }

public List<AppUser> find() { ... }

public Role save(Role role) { ... }

public AppUser addRoleToUser(String username, String roleName) { ... }
}
@RestController
@RequestMapping("/api/v1/user")

@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final TokenService tokenService;

@GetMapping
public ResponseEntity<List<AppUser>> getUsers() { ... }

@PostMapping
public ResponseEntity<AppUser> createUser(@RequestBody AppUser user) { ... }

@PostMapping("role")
public ResponseEntity<AppUser> createRole(@RequestBody Role role) { ... }

@PostMapping("{username}/role/{rolename}")
public ResponseEntity<?> addRoleToUser(@PathVariable("username") String username, @PathVariable("rolename") String roleName) { ... }

@GetMapping("refresh-token")
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException { ... }
}
@Bean
CommandLineRunner run
(UserService userService) {
return args -> { // inserting data after application is up
// roles
userService.save(new Role(null, "ROLE_USER"));
userService.save(new Role(null, "ROLE_ADMIN"));
userService.save(new Role(null, "ROLE_SUPER_ADMIN"));
// users
userService.save(new AppUser(null, "James Kirk", "kirk", "123456", new ArrayList<>()));
userService.save(new AppUser(null, "Spock", "spock", "123456", new ArrayList<>()));
userService.save(new AppUser(null, "Leonard McCoy", "mccoy", "123456", new ArrayList<>()));
userService.save(new AppUser(null, "Montgomery Scott", "scott", "123456", new ArrayList<>()));
// roles -> users
userService.addRoleToUser("kirk", "ROLE_SUPER_ADMIN");
userService.addRoleToUser("kirk", "ROLE_ADMIN");
userService.addRoleToUser("spock", "ROLE_ADMIN");
userService.addRoleToUser("spock", "ROLE_USER");
userService.addRoleToUser("mccoy", "ROLE_USER");
userService.addRoleToUser("scott", "ROLE_USER");
};
}

Security configuration

After authenticate the user by its password, we will use JSON Web Tokens (JWT) for further requests to identify and authorize its access to the operations endpoints.

...
auth:
secret: secret # secret for jwt token
@Service
@RequiredArgsConstructor
public class TokenService {

@Value("${auth.secret}")
private String secret;

public String getTokenFrom(String bearerToken) {
if (bearerToken == null || !bearerToken.startsWith("Bearer "))
throw new IllegalArgumentException("Invalid Headers");
String token = bearerToken.substring("Bearer ".length());
return token;
}

public DecodedJWT getDecodedTokenFrom(String token) {
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
return decodedJWT;
}

public String generateAccessToken(AppUser user) {
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) // expires in 10 min
.withIssuer("Books-API")
.withClaim("roles", user.getRoles().stream().map(Role::getName).collect(Collectors.toList()))
.sign(algorithm);
return accessToken;
}

public String generateAccessToken(UserDetails user) {
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) // expires in 10 min
.withIssuer("Books-API")
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
return accessToken;
}

public String generateRefreshToken(UserDetails user) {
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes());
String refreshToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // expires in 60 min
.withIssuer("Books-API")
.sign(algorithm);
return refreshToken;
}

}
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private AuthenticationManager authenticationManager;
private TokenService tokenService;

public CustomAuthenticationFilter(AuthenticationManager authenticationManager, TokenService tokenService) {
this.authenticationManager = authenticationManager;
this.tokenService = tokenService;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
public class CustomAuthorizationFilter extends OncePerRequestFilter {

private TokenService tokenService;

public CustomAuthorizationFilter(TokenService tokenService) {
this.tokenService = tokenService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/v1/user/refresh-token")) {
filterChain.doFilter(request, response);
return;
}
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
String token = tokenService.getTokenFrom(authorizationHeader);
DecodedJWT decodedJWT = tokenService.getDecodedTokenFrom(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
exception.printStackTrace(); // log error
response.setHeader("error", exception.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_msg", exception.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig
extends WebSecurityConfigurerAdapter {

private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final TokenService tokenService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}

@Override
protected void configure
(HttpSecurity http) throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean(), tokenService);
customAuthenticationFilter.setFilterProcessesUrl("/api/login");
CustomAuthorizationFilter customAuthorizationFilter = new CustomAuthorizationFilter(tokenService);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/v2/api-docs", "/swagger-ui/**", "swagger-ui.html", "/swagger-resources/**").permitAll();
http.authorizeRequests().antMatchers("/api/login/**", "/api/v1/user/refresh-token/**").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/v1").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/v1/user/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/v1/user/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/v1/book/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/v1/book/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.PUT, "/api/v1/book/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.DELETE, "/api/v1/book/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(customAuthenticationFilter);
http.addFilterBefore(customAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
}

@Bean
@Override

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

Testing the application

With all set up, we can run the application to test the operations.

Unauthenticated access error
Login request with token response
Authenticated request with successful response
Refresh token request

Resources and further reading:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store