Securing Spring webservices with JSON Web Token authentication

Properly securing a RESTful webservice can be challenging as REST operations are stateless by nature. Nevertheless, there are various scenarios in which at least a subset of webservice operations need to be secured. JSON Web Tokens are one way of implementing an authentication mechanism for webservices that can be used in conjunction with Spring Security.

In order to get started, we need to use some library that provides convenient JSON Web Token operations. In this example we will use JJWT:

<dependency>  
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>${jjwt.version}</version>
</dependency>  

Configuring Spring Security

Next, we will configure Spring security to allow access to a login method at /api/login and restrict access to all webservice functions with the pattern /api/secure/**:

<!-- API Security -->  
<http pattern="/api/login" security="none"/>

<http pattern="/api/secure/**" entry-point-ref="jwtAuthenticationEntryPoint" create-session="stateless">  
    <csrf disabled="true"/>
    <custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationTokenFilter"/>
</http>

<beans:bean id="jwtAuthenticationSuccessHandler" class="com.lukaspradel.jwt.security.JwtAuthenticationSuccessHandler" />

<beans:bean id="jwtAuthenticationTokenFilter" class="com.lukaspradel.jwt.security.JwtAuthenticationTokenFilter">  
    <beans:property name="authenticationManager" ref="jwtAuthMgr" />
    <beans:property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" />
</beans:bean>

<authentication-manager id="jwtAuthMgr">  
    <authentication-provider ref="jwtAuthenticationProvider" />
</authentication-manager>  

The related classes look like this:

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint,  
        Serializable {

    private static final long serialVersionUID = 3798723588865329956L;

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException {
        // This is invoked when user tries to access a secured REST resource
        // without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no
        // 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}
public class JwtAuthenticationSuccessHandler implements  
        AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication) {
        // Don't do anything specific here
    }

}
public class JwtAuthenticationTokenFilter extends  
        AbstractAuthenticationProcessingFilter {

    @Autowired
    private JwtProperties jwtProperties;

    public JwtAuthenticationTokenFilter() {
        super("/api/secure/**");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) {
        String header = request.getHeader(jwtProperties.getJwtHeader());

        if (header == null || !header.startsWith(jwtProperties.getJwtSchema())) {
            throw new JwtTokenMissingException(
                    "No authentication token found in request headers");
        }

        String authToken = header.substring(jwtProperties.getJwtSchema()
                .length());

        JwtAuthenticationToken authRequest = new JwtAuthenticationToken(
                authToken);

        return getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);

        // As this authentication is in HTTP header, after success we need to
        // continue the request normally
        // and return the response as if the resource was not secured at all
        chain.doFilter(request, response);
    }
}
@Component
public class JwtAuthenticationProvider extends  
        AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private JwtTokenValidator jwtTokenValidator;

    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
    }

    @Override
    protected UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
        String token = jwtAuthenticationToken.getToken();

        CustomUserDetails parsedUser = jwtTokenValidator.parseToken(token);

        if (parsedUser == null) {
            throw new JwtTokenMalformedException("JWT token is not valid");
        }

        return new AuthenticatedUser(parsedUser.getId(),
                parsedUser.getUsername(), token, parsedUser.getAuthorities());
    }

}

JwtAuthenticationEntryPoint and JwtAuthenticationSuccessHandler are straight forward.

The JwtAuthenticationTokenFilter verifies if the request format adheres to the JSON Web Token convention which requires the request Header to look like this:

Authorization Bearer <JSON Web Token>

In this example, we have simply extracted the Authorization and Bearer strings to a properties file.

We will look at the JwtAuthenticationToken class soon, for now we will inspect the JwtAuthenticationProvider. The purpose of this authentication provider is to perform the actual authentication. It takes the JSON Web Token parsed and passed by the JwtAuthenticationTokenFilter and invokes the actual authentication using an injected validator.

The authentication mechanism

The JwtAuthenticationToken only serves to hold the actual token in a way that Spring Security understands:

/**
 * Holder for JWT token from the request.
 * Other fields aren't used but necessary to comply to the contracts of
 * AbstractUserDetailsAuthenticationProvider
 *
 */
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = -6181695412034946378L;

    private String token;

    public JwtAuthenticationToken(String token) {
        super(null, null);
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

Now, let's take a look at the actual JwtTokenValidator:

@Component
public class JwtTokenValidator {

    private static final Logger logger = LoggerFactory
            .getLogger(JwtTokenValidator.class);

    @Autowired
    private JwtProperties jwtProperties;

    @Autowired
    private CredentialsService credentialsService;

    public CustomUserDetails parseToken(String token) {

        CustomUserDetails userDetails = null;

        try {
            Claims body = Jwts.parser()
                    .setSigningKey(jwtProperties.getJwtSecret())
                    .parseClaimsJws(token).getBody();

            // perform credentials check with CredentialsService
        } catch (JwtException e) {
            logger.error("Failed to parse JWT token", e);
        }

        return userDetails;
    }
}

Here, we use the JJWT library to extract the relevant data from the raw JSON Web Token string and then use the CredentialsService to validate the credentials provided in the token. This portion will depend on what data you specify for valid JSON Web Tokens.

Login and token generation mechanism

We have covered the authentication step, however we have not discussed how (valid) tokens are generated in the first step. Recall that we specified the /api/login request path for this:

@RestController
@Transactional
public class WebserviceController {  
    ....
    @Autowired
    private CredentialsService credentialsService;

    @Autowired
    private JwtTokenGenerator jwtTokenGenerator;

    @RequestMapping(value = "/api/login", method = RequestMethod.POST)
    public JwtToken jsonLogin(@RequestBody JwtTokenRequest credentials) {

        JwtToken response = new JwtToken();

        CustomUserDetails userDetails = credentialsService
                .loadUserByUsername(credentials.getUsername());

        if (userDetails == null) {
            response.setError("Bad credentials");
            return response;
        }

        if (credentialsService.verifyCredentials(credentials.getPassword(),
                userDetails.getPassword())) {
            // Generate valid token
            String token = jwtTokenGenerator.generateToken(userDetails.getId(),
                    userDetails.getUsername());
            response.setToken(token);
        } else {
            response.setError("Bad credentials");
        }

        return response;
    }
}

Essentially, we require the user to submit his credentials using a POST request and check these for validity. If all is well, we respond with a valid, freshly generated JSON Web Token. First, let's look at the JwtTokenRequest:

public class JwtTokenRequest {

    private String username;

    private String password;

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

In this example we require the user to simply pass his username and his password.

If all is well, we use the JwtTokenGenerator to generate a valid JSON Web Token:

@Component
public class JwtTokenGenerator {

    @Autowired
    private JwtProperties jwtProperties;

    public String generateToken(Long id, String username) {

        Claims claims = Jwts.claims().setSubject(username);
        // put further claims as needed

        LocalDateTime oneDayFromNow = LocalDateTime.now().plusHours(
                jwtProperties.getJwtValidHours());
        Date expirationDate = Date.from(oneDayFromNow.atZone(
                (ZoneId.systemDefault())).toInstant());

        return Jwts
                .builder()
                .setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512,
                        jwtProperties.getJwtSecret()).compact();
    }
}

Adjust the claims to the data you specified for your JSON Web Tokens. The actual JSON Web Token algorithm is implemented by JJWT. The claims you specify here correspond to the extraction performed in the JwtTokenValidator that we covered earlier.

Finally, the JwtToken POJO that is used to map our response to JSON is again just a holder for the token string:

public class JwtToken {

    private String error;

    private String token;

    public JwtToken() {
    }

    public JwtToken(String token) {
        this.token = token;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

Conclusion

By combining the JSON Web Token authentication mechanism with Spring Security as demonstrated here, the specified subset of webservice functions can only be used by users who include a valid JSON Web Token in their request headers. Thus, all functions retain their stateless nature. The "statefulness" of the login "state" lies in the validity period of the JSON Web Tokens we generate in JwtTokenGenerator.

Overall, JSON Web Tokens provide a convenient authentication mechanism for webservices that can be integrated into Spring Security with little overhead.

Lukas

Read more posts by this author.

Dortmund, Germany https://lukaspradel.com

Subscribe to Programming & Whisky

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!