Spring Security JWT Authentication

Spring May 23, 2020

JSON Web Tokens (JWT) are commonly used for user authentication. It is a token generated by the backend and given to the frontend to be used in the subsequent requests as the session identifier.

JWT tokens are consist of 3 parts separated by dots, as header.payload.signature. Header contains the the token type and the algorithm used to sign, such as HMAC SHA256 or RSA. The payload contains the claims which is the data to be stored in the token, and you can think like key value pairs.

{
  "timestamp": "1234567890",
  "name": "Frodo Baggins",
  "group": "Fellowship",
  "active: true
}

The signature part contains the signature created by the algorithm that is using the header, payload, and a secret key. So the signature is used to verify that the message is not changed on the way by any intruder. So we should keep the secret key private. Read more details on JWT Details: jwt.io. An example token looks like as follows:

After login operation, backend creates such a token and sends back to frontend. In the following requests frontend should send this key for example in the header Authorization in order to recognise the owner of the session. Backend application should verify this token at each API call as follows:

  • Check if the JWT token exists in the Authorization header
  • Validate the token
  • Otherwise reject with permission denied (403)
  • If it is a login call, check the credentials, generate a token and send it back.

Initiate the Spring Project

We initiate our project from Spring Initializr with the web, jpa and security dependincies. We need to make sure that we have the security dependency:

dependencies {
    ...
    compile("org.springframework.boot:spring-boot-starter-security")
}
build.gradle

After that we can run a database for example on docker.

docker run -e POSTGRES_PASSWORD=root -d -p 5432:5432 -v db-data:/var/lib/postgresql/data postgres

And provide the connection details in application.yml

spring:
    datasource:
        password: root
        url: jdbc:postgresql://db:5432/postgres
        username: postgres
    jpa:
        database-platform: org.hibernate.dialect.PostgreSQL9Dialect
        hibernate:
            ddl-auto: update
        properties:
            hibernate:
                dialect: org.hibernate.dialect.PostgreSQLDialect
                temp:
                    use_jdbc_metadata_defaults: false
application.yml

User Registration

Spring provides UserDetails class that can be used for authentication so we need to extend this class and generate our ApplicationUser entity. We can add desired fields for a user and we also need to override the parent's methods.

@Entity
@Data
public class ApplicationUser implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;
    private String name;
    private String email;
    private String phoneNumber;
    private String address;
    private String avatarUrl;
    private String type;

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public Collection <? extends GrantedAuthority> getAuthorities() {
        return null;
    }
}
ApplicationUser.class

In order to interact with the database we can use JPA's JpaRepository. It has all the built in save, update, find, delete methods.  We only need to add findByUsername method that will be need later.

@Repository
public interface ApplicationUserRepository extends JpaRepository <ApplicationUser, Long> {
    ApplicationUser findByUsername(String username);
}

Spring Security also provides built-in UserDetailsService. We only need to extend it override the loadUserByUsername method to configure how the user will. be retrieved. So it gives us the prototype, but implementation is flexible.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private ApplicationUserRepository applicationUserRepository;

    public UserDetailsServiceImpl(ApplicationUserRepository applicationUserRepository) {
        this.applicationUserRepository = applicationUserRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ApplicationUser applicationUser = applicationUserRepository.findByUsername(username);
        if (applicationUser == null) {
            throw new UsernameNotFoundException(username);
        }
        return applicationUser;
    }
UserDetailsService.java

We have our service and repository, now we can create the controller to expose the registration API to our clients.

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final ApplicationUserRepository applicationUserRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public UserController(ApplicationUserRepository applicationUserRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.applicationUserRepository = applicationUserRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @PostMapping("/sign-up")
    public void signUp(@RequestBody ApplicationUser user) {
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        applicationUserRepository.save(user);
    }
}
UserController.java

When we receive the user details to be saved, we are encoding the user password with Bcrypt encoder. We can create BCryptPasswordEncoder bean as follows:

@Configuration
public class BCryptEncoder {
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
BCryptEncoder.java

Now we expose the /api/users/sign-up/ endpoint to our users. We can send user details to this endpoint and it will be saved to database.

User Authentication and Authorization

In this step we will secure our endpoints and configure the spring via security filters to check authorization (if the user is eligible to access the resource) , and also handle the authentication while logging in. First of all let's keep our constants in a class:

public class SecurityConstants {
    public static final String SECRET = "PrivateSecurityKey";
    public static final long EXPIRATION_TIME = 864_000_000; // 10 days
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";
    public static final String SIGN_UP_URL = "/api/users/sign-up";
}
SecurityConstants.java

And then we will apply following steps:

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
    private UserDetailsServiceImpl userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurity(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().
                csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
                .antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager(), userDetailsService))
                // this disables session creation on Spring Security
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

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

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.applyPermitDefaultValues();
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader(SecurityConstants.HEADER_STRING);
        source.registerCorsConfiguration("/**",corsConfiguration);
        return source;
    }

}
WebSecurity.java

We are configuring our endpoints to be private, except the SIGN_UP_URL and actuator which can be used for health checks. So to be able to access rest of the endpoints a user need to have an Authorization header with issued JWT Token. We are adding the Authentication filter and the Authorization filter in the security filter chain. And we are setting our UserDetailsService to be used for checking credentials. Finally we configure the Cors configuration.

  • Create authentication filter that will issue JWTs to users logging in
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {
            ApplicationUser creds = new ObjectMapper()
                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            new ArrayList <>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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


        ApplicationUser principal = (ApplicationUser) auth.getPrincipal();

        String token = JWT.create()
                .withSubject(principal.getUsername())
                .withClaim("userId", principal.getId())
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .sign(HMAC256(SECRET.getBytes()));
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    }
JWTAuthenticationFilter.java

So we are extending UsernamePasswordAuthenticationFilter and implementing its methods:

attemptAuthentication: checks the user credentials

successfulAuthentication: issues the JWT token. Note that it is adding user id and username claims to the token. We can also add any custom data to be stored as a claim inside the token.

  • Create authorization filter to validate requests containing JWTs
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    private UserDetailsServiceImpl userDetailsService;

    JWTAuthorizationFilter(AuthenticationManager authManager, UserDetailsServiceImpl userDetailsService) {
        super(authManager);
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(HEADER_STRING);

        if (header == null || header.endsWith("null")) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            Long userId = JWT.require(Algorithm.HMAC256(SECRET.getBytes()))
                    .build()
                    .verify(token.replace(TOKEN_PREFIX, ""))
                    .getClaim("userId").asLong();

            if (userId > 0) {
                ApplicationUser user = userDetailsService.getApplicationUserById(userId);
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList <>());
            }
            return null;
        }
        return null;
    }
}
JWTAuthorizationFilter.class

For this one we are extending the BasicAuthenticationFilter  and implementing its method doFilterInternal. This method is checking if there is a HEADER_STRING, which is Authorization in the request. If there is no header or the token is null, it calls. chain.doFilter(req, res); to continue the filter chain and returns. If there is a token in the header our getAuthentication method tries to verify the token with using the SECRET key and parses the token.  

We can retrieve our claims in here.  For example with retrieving userId from the token we can find the user details from the database and set as principal. Spring keeps the principal object in the Security context as the current user. Later on in our services if we need to get the user who makes the call we can retrieve it from Security context as follows:

ApplicationUser principal = (ApplicationUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

Demo

As the spring security automatically exposes the endpoint /login we can register a user and try to login.

  • Register User
Register User
  • Login. After Successful login we receive JWT in the response header
  • If we try to access a private endpoint without sending JWT,  security filters will complain and do not allow as the request is not authorized
  • If we access a private endpoint with the issued JWT, we will be allowed as we are authorized, and our JWT token proves our session.

So we can store the JWT issued after the login inside the cookies or local storage at our frontend application and send for each request.

Bare in mind that there are some drawback of using JWT as Auth Token. As we are not storing tokens in the database,  in other words we do not store the user sessions but only verify if the received token is valid or not, we can not invalidate the tokens. So if we ban a user, or delete user its token will be still valid. If the user takes the token manually from the local storage or cache content and repeat the call with supplying the token it will be authorized. So we have to wait until token timeouts.

If we store the token in database and control it while checking auth. then it brings extra complexity of making database call per request. And also as JWT tokens are long it takes relatively big space in the database, so it is also not very efficient to store it. Moreover verify the token consumes time as well for each request. So if we use JWT authentication for internal service communication it would be efficient to verify token without making db calls and storing the sessions, but we may face the listed problems for using REST calls open to your clients. Refer to Link for more details about the drawbacks.

Tags