Thursday, May 28, 2020

Token JWT and Spring Boot

Objectives

Today I would like to extend my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html) and add protection API based on JWT Token. At the beginning I would like to briefly describe the idea of tokens. Below is added BPMN process flow which shows the end-to-end path (this is a simple process flow without filters, authentication controllers or authentication managers)












The token structure
Token consists of three elements:
  • Header (algorithm and type) - {"alg":"HS256"}
  • Payload  - {"sub":"user","exp":1590655192}
  • Signature (defined secret was used to create signature)


Application

I created application based on my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html).  The main goal is to create JWT token to protect communication between server and clients. 




I selected files which I added or replaced. Let's examine the application's code. 


pom.xml

  <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>  
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-openid</artifactId>  
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</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.1</version>
</dependency>



UserDTO:

@Data
public class UserDTO {
    private String username;
    private String password;
}



SecurityConfig.class


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
        public static final String prefix = "Bearer";
public static final String header = "Authorization";
        public static final String secret = Base64.getEncoder().encodeToString("artsci".getBytes());;
        public static final Long expir = new Long(3600000);

@SuppressWarnings("deprecation")
@Bean
    public UserDetailsService userDetailsService() {
        User.UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user").password("user").roles("USER").build());
        manager.createUser(users.username("admin").password("admin").roles("USER", "ADMIN").build());
        return manager;
    }

   @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
}
   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.csrf().disable()   
        .authorizeRequests()
        .antMatchers(HttpMethod.POST, "/login").permitAll()
        .anyRequest().authenticated()
        .and().addFilterBefore(new LoginFilter("/login", authenticationManager()),   
                 UsernamePasswordAuthenticationFilter.class)
        .addFilterBefore(new AuthJwtFilter(), UsernamePasswordAuthenticationFilter.class);
        http.headers().cacheControl();
    }


LoginFilter.class

public class LoginFilter extends AbstractAuthenticationProcessingFilter {
    public LoginFilter(String url, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authenticationManager);
   }

   @Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
           response)
throws AuthenticationException, IOException, ServletException {
    UserDTO user = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
     UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                                                                                                 user.getUsername(),
                                                                                                 user.getPassword(),
                                                                                                 Collections.emptyList());

     return getAuthenticationManager().authenticate(token);
}

   @Override

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
          FilterChain chain,
Authentication authResult) throws IOException, ServletException {

     String token = Jwts.builder()
            .setSubject(authResult.getName())         
            .signWith(SignatureAlgorithm.HS256, secret)
            .setExpiration(new Date(System.currentTimeMillis() + expir))
            .compact();
     response.addHeader(header, prefix + " " + token);    
     response.setContentType("application/json");
     response.setCharacterEncoding("UTF-8");
     response.getWriter().write("{\"" + header + "\":\"" + prefix + " " + token + "\"}");     
}
}

AuthJwtFilter.class

public class AuthJwtFilter extends GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
Authentication auth = null;
         if(!((HttpServletRequest) request).getHeader(header).isEmpty()) {
String claim = ((HttpServletRequest)request).getHeader(header) 
                             .replace(prefix,"").substring(1);
String user = Jwts.parser().setSigningKey(secret).parseClaimsJws(claim)
                        .getBody().getSubject();
auth = new UsernamePasswordAuthenticationToken(user, null,                                      
                             Collections.emptyList());

}
       
         SecurityContextHolder.getContext().setAuthentication(auth);
         chain.doFilter(request,response);
}
}

The results

So, our application should work correctly. Let's try to call protected API. At the beginning it is necessary to to generate token. To achieve this goal I attempt to use URL:  "/login" and pass my credential. Below is example of CURL's request.


curl --location --request POST http://localhost:8080/login --header "Content-Type: application/json" --data-raw "{\"username\":\"user\",\"password\":\"user\"}"
{"Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8"}

Fantastic, JWT Token was generated. Let's try to call "/regions" service using previously generated token.


curl -H "Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8" --request GET http://localhost:8080/regions/
[{"regionId":1,"name":"Europe"},{"regionId":2,"name":"Americas"},{"regionId":3,"name":"Asia"},{"regionId":4,"name":"Middle East and Africa"}]

It seems that the solution works exactly as I expected :)

No comments:

Post a Comment