SpringBoot and JWT security

When creating an API you can manage a “session” with a JWT Json Web Tokens ensuring the user has login previously.

This post explains how JWT is working and how to implement it with SpringBoot using user identity stored in a Database.

This is based on a real implementation and is the sum of lots of search on Internet to make it correctly working.

How does JWT works

Basically JWT is a Token obtain from a server after a login. Once obtained, this token, can be used to identified in the next requests. The server does not need to verify the identity from the database as the Token contains all the informations you need and is certified.

The scenario is the following one :

jwt design – from https://www.toptal.com/java/rest-security-with-jwt-spring-security-and-java

In the step 1 the browser sent a login request with a user Id and a Password. This can be sent as a POST message in the Body or as a Header like for a Basic authentication.

In the step 2 the server verify the identity of the user verifying its existence in the database and the conformity of the password. At this step the server also get the Roles this user have. At this point is can extract any information later needed to add it into the token.

A token is created with the following format :

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm
FtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6IjhjNWVmNjg4LWZkMWUtND
lmMy1iZDUxLTdlNDMyMDYzZGVhMCIsImlhdCI6MTUxMjMzNTQ3MywiZXhwIjoxNTEyM
zM5MDczfQ.bOtJw6ejegarrfnUFEMSBvjr_GBEl3mmzdC8cy4-7-M

There are 3 parts encoded Base64:

  • The blue one is the header it contains the type of token and the hashing algorithm used.
{
 "typ": "JWT",
 "alg": "HS256"
}
  • The red one is the body, it contains the information about the user and the token validity
{
 "sub": "1234567890",
 "name": "John Doe",
 "roles": [ "ROLE_ADMIN", "ROLE_USER" ] ,
 "jti": "8c5ef688-fd1e-49f3-bd51-7e432063dea0",
 "iat": 1512335473,
 "exp": 1512339073
}

It cans contains any information but some are mandatory:

  • sub : is the subject = the user authentiated
  • exp : is the expiration date (timestamp in ms)

Others are settings set by the serveur for its internal use:

  • roles : is the list of role the user have in the application
  • name : is the full name of the user
  • The last (Green) part is the more important. This is the signature of the Token. It is basically a HMACSHA256 hash obtained from the header + body with a secret only knows by the application. If no-one know the secret it is not possible for anyone to create a valid token. (So be careful, if known, anyone can create a valid token for anyone…)

In the Step 3 the Token is return to the browser as a Header identified by

Authorization Bearer eyJ0eXAiOiJ...

The browser can read it and store it to reuse it on each of the next communications.

In the Step 4 the browser wants to request something to the server. The browser will create a GET/POST request adding the Token as a Authorization Bearer … in the header of the request.

In the Step 5 the server read the Token in the header, verify the signature of the token using its secret key. If the signature is ok, it verify the Token expiration. When ok it extracts the informations from the token like the UserName, the UserRoles …  As you can see we do not need to verify the User existence and right from the database / LDAP …

In the Step 6 the response is sent to the browser. The server can verify the rights, extract the information and prepare the response.

Now we can take a look on how to implement it with SpringBoot.

Configure Security in Spring-Boot

The first step is to add as a dependency the web-security package. This is done by adding the following line to the build.gradle file.

compile("org.springframework.boot:spring-boot-starter-security")
compile("io.jsonwebtoken:jjwt:0.7.0")

The first one is generic to web security and the second one is related to the JWT token creation.

Then I have based my JWT configuration mostly to this blog post (https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/) and made many modifications to match a real case use.

The first Class to be created is the Configuration entity defining the security rules:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   private UserService userService;

   @Autowired
   private WebSecurityConfig webSecurityConfig;

   @Override 
   protected void configure(HttpSecurity http) throws Exception {

    // Create the Authentication filter
    JWTAuthenticationFilter autheFilter = 
     new JWTAuthenticationFilter(
     authenticationManager(),
     userService,
     webSecurityConfig
    );
    // Specify the login url handler (default is /login)
    // this line allows to change it.
    autheFilter.setRequiresAuthenticationRequestMatcher(
     new AntPathRequestMatcher("/sign/in", "POST")
    );

    // Create the Authorization Filter
    JWTAuthorizationFilter authoFilter = 
      new JWTAuthorizationFilter(
      authenticationManager()
    );
    // Inject the configuration Class into the Filter
    authoFilter.setWebSecurityConfig(webSecurityConfig);


    http.cors().and().csrf().disable().authorizeRequests()
     // The following url patter does not requires user
     // to be authenticated for accessing .antMatchers("/").permitAll()
     .antMatchers("/public/**").permitAll() 
     .antMatchers(HttpMethod.POST, "/sign/in").permitAll()
     // Other URL patterns requires authentication 
     .anyRequest().authenticated() 
    .and()
     // The authentication filter 
     .addFilter(autheFilter)
     // The authorization filter
     .addFilter(authoFilter)
     // this disables session creation on Spring Security 
     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 
} 
@Override 
protected void configure(AuthenticationManagerBuilder auth) 
          throws Exception 
{ 
   auth.userDetailsService(userService); 
} 

}

Class decoration

  • @Configuration allows to identify the class as a Configuration class and inject in this class the needed dependencies like in this example the @Autowired userService
  • @EnableWebSecurity activate the websecurity in SpringBoot
  • @EnableGlobalMethodSecurity(prePostEnabled=true) this is activating the RoleBased authorization used later with @PreAuthorize decoration on API.

Injected Services

  • UserService (implements UserDetailsService) is used to managed the user stored in the database and link it to the security.
  • WebSecurityConfig is the configuration class extracting the configuration to generate the token from the application.properties file.

Now we can create the Authentication

The Authentication is verifying the login and password when receiving a request, then it creates a Token for this request. The Token contains the UserName, the Roles and whatever we need. In the exemple I had groups to add, consider it as a custom strings.

@Configurable
public class JWTAuthenticationFilter 
       extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private UserService userService;
    private WebSecurityConfig config;

    public JWTAuthenticationFilter(
              AuthenticationManager authenticationManager,
              UserService userService,
              WebSecurityConfig config) {
        this.authenticationManager = authenticationManager;
        this.userService = userService;
        this.config = config;
    }

The Authentication class use the UserService to make the link with the User stored in the database. The WebSecurityConfig provides the information to create the Token.

The Next methods is called when the login (/sign/in) page is called to verify the identify of the user.

@Override
public Authentication attemptAuthentication(
          HttpServletRequest req,
          HttpServletResponse res) throws AuthenticationException {

        // This Class is an image of the JSON object contained in 
        // request Body. Not more.
        AccountCredentials creds = null;
        try {
            // Try to read the body to get the Username / Password
            creds = new ObjectMapper()
                    .readValue(req.getInputStream(), AccountCredentials.class);
        } catch (IOException e) {
            creds = null;
        }

        if ( creds == null ) {
            // If Username / Password not present in body we
            //   try to extract the informations from the header
            //   following Basic Authorization standards
            String v = req.getHeader("Authorization");
            if ( v != null && v.startsWith("Basic ") ) {
                v = v.substring(6);
                v = new String(Base64.decode(v.getBytes()));
                StringTokenizer st = new StringTokenizer(v,":");
                if ( st.countTokens() == 2 ) {
                    creds = new AccountCredentials();
                    creds.setUsername(st.nextToken());
                    creds.setPassword(st.nextToken());
                }
            }
        }
        if (    creds != null 
             && creds.getUsername() != null 
             && creds.havePassword() ) {
            // The Username & Password are passed to authenticate
            // method. This one will verify the compliance
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername().toLowerCase(),
                            creds.getPassword(userService),
                            creds.getAuthorities(userService))
            );
        // In any error case, return something that will be invalid
        } else return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        "none",
                        "none",
                        new ArrayList<>()
                )
        );
}

In the previous code AccountCredentials is storing username & password as received from the Request. But during the Authentication process the password must be verified. The password string must be returned as in the compared user element stored in the database (basically hashed). So in this Class the method getPassword will hash the password before returning it, the same way it is in the DB. This is the reason why the getPassword method pass the userService object. It’s to ba able to call the needed methods for this.

The next method is called when the identity has been verified for the token generation:

@Override
protected void successfulAuthentication(
     HttpServletRequest req,
     HttpServletResponse res,
     FilterChain chain,
     Authentication auth
) throws IOException, ServletException {

   // The Auth Mechanism stores the Username the Principal.
   // The username is stored in the Subject field of the Token
   String login = ((UserDetails)auth.getPrincipal()).getUsername();
   Claims claims = Jwts.claims().setSubject(login);
   
   long expirationTime = config.getUserExpiration();
   if ( login != null && login.length() > 0 ) {
    // From the user name we can retreive the User in the DB
    UserElement e = userService.findUserByLogin(login, false);
    // To extract the roles (String[]) and the groups (String[])
    claims.put("roles", userService.getAclForExistingUser(login));
    claims.put("groups", e.getGroups());
   }
   // Now we can generate the token
   String token = Jwts.builder()
      .setClaims(claims)
      .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
      .signWith(SignatureAlgorithm.HS512, config.getJwtSecret().getBytes())
      .compact();
   res.addHeader(
          WebSecurityConfig.HEADER_STRING, // Basically "Authorization"
          WebSecurityConfig.TOKEN_PREFIX + token // "Bearer "
    ); 
}

The Authorization step

The next step is Authorization : we are receiving a token in the header of the request, verify this token to allow the user to connect. We configure Roles & Group from the Token.

The first part initialize the filter and filter the request to verify the Token existance and validity.

public class JWTAuthorizationFilter 
     extends BasicAuthenticationFilter {

    private WebSecurityConfig webSecurityConfig;

    // Generic Constructor needed 
    public JWTAuthorizationFilter(AuthenticationManager authManager) {
        super(authManager);
    }

    // Inject the configuration into the Filter
    public void setWebSecurityConfig(WebSecurityConfig webSecurityConfig) {
        this.webSecurityConfig = webSecurityConfig;
    }

    // Process the Request to extract the Token
    @Override
    protected void doFilterInternal(
         HttpServletRequest req,
         HttpServletResponse res,
         FilterChain chain
    ) throws IOException, ServletException {

        String header = req.getHeader(WebSecurityConfig.HEADER_STRING);

        if (   header == null 
            || !header.startsWith(WebSecurityConfig.TOKEN_PREFIX)) {
            // Token not found - leave
            chain.doFilter(req, res);
            return;
        }
        // Token Found - GetIt and Authenticate
        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

This part is now processing the authentication. It has been called previously. The token will be proceeded to extract the contained information and create an Authenticated User you will be able to access in the Request as Principal with the Roles correctly associated.

private UsernamePasswordAuthenticationToken getAuthentication(
     HttpServletRequest request
) {
    String token = request.getHeader(WebSecurityConfig.HEADER_STRING);
    if (token != null) {
        // parse the token.
        Claims claims = Jwts.parser()
                .setSigningKey(webSecurityConfig.getJwtSecret().getBytes())
                .parseClaimsJws(token.replace(WebSecurityConfig.TOKEN_PREFIX, ""))
                .getBody();
        // Extract the UserName
        String user = claims.getSubject();

        // Extract the Roles
        ArrayList<String> roles = (ArrayList<String>)claims.get("roles");
        // Then convert Roles to GrantedAuthority Object for injecting
        ArrayList<MyGrantedAuthority> list = new ArrayList<>();
        if ( roles != null ) {
            for (String a : roles) {
                MyGrantedAuthority g = new MyGrantedAuthority(a);
                list.add(g);
            }
        }
        // Attached the groups to the request attributes for a later use
        //  (custom need may be not needed for you)
        request.setAttribute("groups", (ArrayList<String>)claims.get("groups"));

        // Return an Authenticated user with the list of Roles attached
        if (user != null) {
            return new UsernamePasswordAuthenticationToken(user, null, list);
        }
        return null;
    }
    return null;
}

Apply in your API

Now we can create API with security applied. There are two ways to refer the User in the Api :

You can use the @PreAuthorize decoration, associated with hasAuthority and the desired Role name. As you can see the full name is needed for the role:

@RequestMapping(value="/test1", method= RequestMethod.GET)
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<?> test1() {
    return new ResponseEntity<>("ok !!", HttpStatus.OK);
}

The second solution is to code this:

You can see in the following exemple that the Role name does not contains ROLE_ … it means to work with this solution you need to have all your role starting by ROLE_

@RequestMapping(value="/test2", method= RequestMethod.GET)
public ResponseEntity<?> test2(HttpServletRequest request) {
  if ( request.isUserInRole('ADMIN') ) {
 
    Principal userPrincipal = request.getUserPrincipal();
    log.info(userPrincipal.getName());

    ArrayList<String> groups = 
          (ArrayList<String>)request.getAttribute("groups");
    for ( String s : groups) {
        log.info("part of group: "+s);
    }
  }
  return new ResponseEntity<>("ok !!", HttpStatus.OK);
}

 

 

 

This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *