In this article, we will be creating a sample REST CRUD APIs and provide JWT role based authorization using spring security to these APIs. We will be using spring boot 2.0
and JWT 0.9.0
. In the DB, we will have two roles defined as ADMIN and USER with custom UserDetailsService implemented and based on these roles the authorization will be decided. We will be using spring data to perform our CRUD operations and spring provided annotations such as @PreAuthorize
, @Secured
and @EnableGlobalMethodSecurity
for authorization. At the end, we will be testing it with postman.
In my previous articles, we have discussed a lot about Spring Boot JWT Auth, JWT Angular Auth and Spring Boot JWT OAuth but in these cases, we had hardcoded the user role in the code and it does not provide much sense in real-time applications. But it definitely provides a good way to get started. Hence, in this article let us have a full-fledged project to provide JWT role-based authorization to REST APIs.
JSON Web Token
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object,a stateless authentication mechanism as the user state is never saved in server memory.A JWT token consists of 3 parts separated with a dot(.) i.e. Header.payload.signature. Following is a sample JWT token.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsInNjb3BlcyI6IlJPTEVfQURNSU4iLCJpYXQiOjE1MjYzOTA0NDMsImV4cCI6MTUyNjQwODQ0M30.4uWqKGkyP7TJu_W2M0apZqK6CLrM8bgl3uolo2piHmQ
Maven Dependency
Following is the maven depencency that we have defined in our pom.xml.
pom.xml<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies>
Spring Security Config
Now, let us define WebSecurityConfig.java that has all the configuration related to our spring security.Here, we have configured that no authentication is required to access the url /token
, /signup
and rest of the urls are secured. Here prePostEnabled = true enables support for method level security and enables use of @PreAuthorize
and similarly to enable @Secured
annotation we require securedEnabled = true.
@Configuration @EnableWebSecurity //@EnableGlobalMethodSecurity(securedEnabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(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.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/token/*", "/signup").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } }
Following is our AuthenticationController that has API exposed to generate JWT token.
AuthenticationController.java@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/token") public class AuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenProvider jwtTokenUtil; @RequestMapping(value = "/generate-token", method = RequestMethod.POST) public ResponseEntity> register(@RequestBody LoginUser loginUser) throws AuthenticationException { final Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginUser.getUsername(), loginUser.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); final String token = jwtTokenUtil.generateToken(authentication); return ResponseEntity.ok(new AuthToken(token)); } }
Following is th snippet from TokenProvider that is responsible for validating user and generating JWT token.
TokenProvider.javapublic String generateToken(Authentication authentication) { final String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .signWith(SignatureAlgorithm.HS256, SIGNING_KEY) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000)) .compact(); } UsernamePasswordAuthenticationToken getAuthentication(final String token, final Authentication existingAuth, final UserDetails userDetails) { final JwtParser jwtParser = Jwts.parser().setSigningKey(SIGNING_KEY); final JwsclaimsJws = jwtParser.parseClaimsJws(token); final Claims claims = claimsJws.getBody(); final Collection extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); }
JWT Authentication
Following class extends OncePerRequestFilter that ensures a single execution per request dispatch. This class checks for the authorization header and authenticates the JWT token and sets the authentication in the context. Doing so will protect our APIs from those requests which do not have any authorization token. The configuration about which resource to be protected and which not can be configured in WebSecurityConfig.java
JwtAuthenticationFilter.javapublic class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private TokenProvider jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(HEADER_STRING); String username = null; String authToken = null; if (header != null && header.startsWith(TOKEN_PREFIX)) { authToken = header.replace(TOKEN_PREFIX,""); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } 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(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(req, res); } }
Spring MVC Configuration
Now let us define our model classs - User and Role. It has many to many relationship. Visit here for detail explanation on hibernate many to many relationship.
User.java@Entity public class User { @Id @GeneratedValue(strategy= GenerationType.AUTO) private long id; @Column private String username; @Column @JsonIgnore private String password; @Column private long salary; @Column private int age; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinTable(name = "USER_ROLES", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") }) private SetRole.javaroles; public User(){ } public User(long id){ this.id = id; } //setters and getters
@Entity public class Role { @Id @GeneratedValue(strategy= GenerationType.AUTO) private long id; @Column private String name; @Column private String description; //setters and getters
Now, let us define the controller. The method listUser() can only be accessed by admin.We have used multiple veriations of authorization here. Since, we have prePostEnabled = true, we are using @PreAuthorize annotation. We can also use @Secured at controller methods but for that we require securedEnabled = true in WebSecurityConfig.java
@CrossOrigin(origins = "*", maxAge = 3600) @RestController public class UserController { @Autowired private UserService userService; //@Secured({"ROLE_ADMIN", "ROLE_USER"}) @PreAuthorize("hasRole('ADMIN')") @RequestMapping(value="/user", method = RequestMethod.GET) public ListlistUser(){ return userService.findAll(); } //@Secured("ROLE_USER") //@PreAuthorize("hasRole('USER')") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) public User getOne(@PathVariable(value = "id") Long id){ return userService.findById(id); } @RequestMapping(value="/signup", method = RequestMethod.POST) public User create(@RequestBody UserDto user){ return userService.save(user); } @PreAuthorize("hasRole('ADMIN')") @RequestMapping(value="/user/{id}", method = RequestMethod.DELETE) public User deleteUser(@PathVariable(value = "id") Long id){ userService.delete(id); return new User(id); } }
Following is the UserServiceImpl.java
@Service(value = "userService") public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserDao userDao; @Autowired private BCryptPasswordEncoder bcryptEncoder; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findByUsername(username); if(user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority(user)); } private SetgetAuthority(User user) { Set authorities = new HashSet<>(); user.getRoles().forEach(role -> { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); }); return authorities; } public List findAll() { List list = new ArrayList<>(); userDao.findAll().iterator().forEachRemaining(list::add); return list; } @Override public void delete(long id) { userDao.deleteById(id); } @Override public User findOne(String username) { return userDao.findByUsername(username); } @Override public User findById(Long id) { return userDao.findById(id).get(); } @Override public User save(UserDto user) { User newUser = new User(); newUser.setUsername(user.getUsername()); newUser.setPassword(bcryptEncoder.encode(user.getPassword())); newUser.setAge(user.getAge()); newUser.setSalary(user.getSalary()); return userDao.save(newUser); } }
SQL Scripts
The passwords are Bcrypted as password1, password2, password3 for corresponding users user1, user2, user3. Also, you can use online Bcrypt tool to generate your own bcrypt password.
INSERT INTO user (id, username, password, salary, age) VALUES (1, 'user1', '$2a$04$Ye7/lJoJin6.m9sOJZ9ujeTgHEVM4VXgI2Ingpsnf9gXyXEXf/IlW', 3456, 33); INSERT INTO user (id, username, password, salary, age) VALUES (2, 'user2', '$2a$04$StghL1FYVyZLdi8/DIkAF./2rz61uiYPI3.MaAph5hUq03XKeflyW', 7823, 23); INSERT INTO user (id, username, password, salary, age) VALUES (3, 'user3', '$2a$04$Lk4zqXHrHd82w5/tiMy8ru9RpAXhvFfmHOuqTmFPWQcUhBD8SSJ6W', 4234, 45); INSERT INTO role (id, description, name) VALUES (4, 'Admin role', 'ADMIN'); INSERT INTO role (id, description, name) VALUES (5, 'User role', 'USER'); INSERT INTO user_roles (user_id, role_id) VALUES (1, 4); INSERT INTO user_roles (user_id, role_id) VALUES (2, 5);
Project Structure
This will be simple project structure. We will have all the base package as com.devglan and all the security configs inside config folder. Similarly, all the controllers, service and DAO will be in their corresponding folder of controller, service and dao. Following will be the final structure.
API Details
API to create new user
URL: http:localhost:8080/signup
Method: POST
Payload: { "username": "user4", "password": "password5", "age": 33, "salary": 898999 }
API to generate token
URL: http:localhost:8080/token/generate-token
Method: POST
Payload: { "username": "user4", "password": "password5" }
API to Fetch All User(ADMIN Role)
URL: http:localhost:8080/user
Method: GET
Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsInNjb3BlcyI6IlJPTEVfQURNSU4iLCJpYXQiOjE1MjYzOTA0NDMsImV4cCI6MTUyNjQwODQ0M30.4uWqKGkyP7TJu_W2M0apZqK6CLrM8bgl3uolo2piHmQ
API to Fetch SingleUser(USER or ADMIN Role)
URL: http:localhost:8080/user/1
Method: GET
Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsInNjb3BlcyI6IlJPTEVfQURNSU4iLCJpYXQiOjE1MjYzOTA0NDMsImV4cCI6MTUyNjQwODQ0M30.4uWqKGkyP7TJu_W2M0apZqK6CLrM8bgl3uolo2piHmQ
Conclusion
In this article, we learned about creating a sample REST CRUD APIs and provide JWT role based authorization using spring security to these APIs. The source can be downloaded from the github link.If you liked this post, I would love to hear back in the comment section.