Deep Dive to OAuth2.0 and JWT (Part 4 JWT Use Case)
Up your Spring Security game with this tutorial!
Join the DZone community and get the full member experience.
Join For FreeScenario
Assume that you are building an application for a hypothetical store chain. Each user of this application is assigned a role, and each role has a defined set of activities that it can perform (technically the API that it can access). Let say this store has the following roles and activities. (Note: this is part our in a series on JWTs security best-practices, parts one, two, and three can be found here, here, and here, respectively.)
- Admin
- Can add new stores.
- Can add new users and assign roles to them (store admin and store user).
- Store Manager
- Can add new products to the store.
- Can remove products from the store.
- Can update product details.
- User
- Can view his/her detail.
- Can view all products.
- Can view a product using product id.
- Can get all products from a store.
Environment
We will be implementing authentication with the following tools:
- Spring Boot 2: Web and Security.
- JJWT: JWT library for Java and Android.
- H2 : in-memory database for our application.
You may also like: Spring Security Authentication.
Solution
The need here is to implement access control based on roles. So, one of the possible solutions could be to group our API based on use and allow access only to that API group, which has the allowed role. We can manage the role in JWT, and for each request, we can validate the role against the API based on which user is trying to access it.
API
If we try to map these requirements to APIs, one of the possible solutions could be to group the APIs as follows:
- The API that lets the user login, which is accessible to everyone, will return a JWT token in case of a successful login. It will return an HTTP status 401 (Unauthorized) in case of login failure.
- The APIs where you can perform all the operations related to products should only be accessible to users having role the of MANAGER. Let us group them under the URL "/product".
- The APIs using that allows you to add users and stores should only be accessible to users having role ADMIN. Let us group them under the URL "/mgmt".
- Users who can view all products or by product id or by store id will be grouped under the URL, "/user".
Below could be the possible APIs that can be implemented for the above requirements.
Components Required
To add authorization, we need to have below components.
- Add a filter that will extract the "Authorization" token from each request and set it to an instance of org.springframework.security.core.Authentication (Spring's Security context). This filter should be configured to run before
UsernamePasswordAuthenticationFilter
. - An implementation of org.springframework.security.core.userdetails.UserDetails will act as the principal object and will be available in the Spring Security context for each validated request.
- An implementation of org.springframework.security.core.userdetails.UserDetailsService will manage the logic to create and set the Principal of users.
- A JWT token helper will perform operations related to JWT, such as create, validate, or extract information based on the given token.
- Finally, we will need the security configurations to configure allow and deny rules.
Time to Code
You can find the full code on GitHub.
JWT Filter: AUTH token extraction and validation logic. This will be executed for each secured request.
package org.sk.security.demo.config.security;
import java.io.IOException;
import java.security.Principal;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
/**
* @author satish sharma
*
* Filters incoming requests and installs a Spring Security principal if a header corresponding to a valid user is
* found.
*/
public class JWTFilter extends GenericFilterBean {
private TokenService jwtTokenProvider;
public JWTFilter(TokenService jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
/**
* Extract Authorization header and validate token.
* IF token is valid the set the {@link Principal}
*/
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
throws IOException, ServletException {
String token = jwtTokenProvider.getTokenValue(((HttpServletRequest)req).getHeader(HttpHeaders.AUTHORIZATION));
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(req, res);
}
}
JWT Configurer: Configure the JWTFilter to run before the other filters.
package org.sk.security.demo.config.security;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
*
* @author satish sharma
*
* Configure {@link JWTFilter} in the security filters chain
*
*/
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenService jwtTokenProvider;
public JwtConfigurer(TokenService jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(jwtTokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
UserDetail Service: The logic to find out user details and set the Principal.
package org.sk.security.demo.config.security;
import java.security.Principal;
import org.sk.security.demo.db.repositories.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
*
* @author satish sharma
*
* Find and configure user details to be available as {@link Principal}
* */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepo;
public UserDetailsServiceImpl(UserRepository userRepo) {
this.userRepo = userRepo;
}
/**
* User Principal finding logic.
*/
@Override
public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
return this.userRepo.findByLoginId(loginId)
.map(UserPrincipal::new)
.orElseThrow(() -> new UsernameNotFoundException("LoginId: " + loginId + " not found"));
}
}
TokenService: The service class containing logic to generate and validate a token using the JWT library.
package org.sk.security.demo.config.security;
import java.security.Principal;
import java.util.Base64;
import java.util.Date;
import javax.annotation.PostConstruct;
import org.sk.security.demo.db.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.log4j.Log4j2;
/**
*
* @author satish sharma
*
* Help with JWT related operations
*
*/
@Log4j2
@Component
public class TokenService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.tokenValidityInMinutes}")
private long tokenValidityInMinutes;
@Autowired
private UserDetailsService customUserDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(User usr) {
Date validity = new Date(new Date().getTime() + (tokenValidityInMinutes * 10000));
return Jwts.builder().setSubject(usr.getLoginId()).claim("ROLE", usr.getRole())
.claim("NAME", usr.getFirstName()).signWith(SignatureAlgorithm.HS512, secretKey).setExpiration(validity)
.setIssuer("MYAPP").compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = this.customUserDetailsService.loadUserByUsername(getLoginId(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getLoginId(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return (claims.getBody().getExpiration().before(new Date())) ? false : true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public String getTokenValue(String authHeaderValue) {
return (StringUtils.hasText(authHeaderValue) && authHeaderValue.startsWith("Bearer ")) ? authHeaderValue.substring(7, authHeaderValue.length()): null;
}
}
UserPrincipal: The principal object created from our User entity.
package org.sk.security.demo.config.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.sk.security.demo.db.entities.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
*
* @author satish sharma
*
* UserPrincipal to be available in SecurityContext
*/
public class UserPrincipal implements UserDetails {
private final User user;
public UserPrincipal(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(this.user.getRole()));
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getFirstName();
}
@Override
public boolean isAccountNonExpired() {
return !user.isActive();
}
@Override
public boolean isAccountNonLocked() {
return !user.isActive();
}
@Override
public boolean isCredentialsNonExpired() {
return !user.isActive();
}
@Override
public boolean isEnabled() {
return user.isActive();
}
}
Security Config: The @Configuration to stitch all our logic and plug it with Spring Security. This will also contain our security rules to be executed on requests.
package org.sk.security.demo.config.security;
import static org.sk.security.demo.constants.RoleConstants.ROLE_ADMIN;
import static org.sk.security.demo.constants.RoleConstants.ROLE_STORE_MANAGER;
import static org.sk.security.demo.constants.RoleConstants.ROLE_STORE_USER;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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;
/**
* @author satish sharma
*
* Configure SpringSecurity to use our logic for Authorization.
* Also confifure the API accessible to roles
*
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
TokenService jwtTokenProvider;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// Configure resources to be accessible to all H2 console and swagger ui
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/bower_components/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs")
.antMatchers("/webjars/**")
.antMatchers("/")
.antMatchers("/db-console/**.css")
.antMatchers("/db-console/**")
;
}
/**
* Configure security based on roles
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/authorize").permitAll()//allowed to all
.antMatchers("/product/**").hasRole(ROLE_STORE_MANAGER)//allowed only to MANAGER
.antMatchers("/mgmt/**").hasAnyRole(ROLE_ADMIN)//allowed only to ADMIN
.antMatchers("/user/**").hasAnyRole(ROLE_STORE_USER) //allowed only to USER
.anyRequest().authenticated()// Rest of the request must be authenticated
.and()
.apply(new JwtConfigurer(jwtTokenProvider)) //user this to configure filters
;
}
}
Let's Run Some Tests
- Try to view products without authorization.
Trying to view product without authorization - Authenticate: Let us authenticate using with user manager which has MANAGER role.
Logging in as a manager - Get all products: Try to view all products.
Viewing all products as manager - Negative Test: Try to access an API which manager does not have access. In our case, we want to view all stores that are in the "mgmt" bucket and only accessible to the ADMIN role. Here, we get an HTTP 403 (Access Denied) response.
It is clear from the above test that we were able to achieve the RBAC what we wanted. At first, this looks complicated, but once you understand the components and how those roles allow/deny access to the APIs, it becomes very easy to implement this.
You can find the full code at this Github repo. Please feel free to share your valuable views, suggestions, and questions (if any). I would be happy to help!
Further Reading
Opinions expressed by DZone contributors are their own.
Trending
-
Fun Is the Glue That Makes Everything Stick, Also the OCP
-
Working on an Unfamiliar Codebase
-
Integration Architecture Guiding Principles, A Reference
-
DevOps Midwest: A Community Event Full of DevSecOps Best Practices
Comments