Skip to content

Tutorial: Speedment Stream Filters Using JWT Data

Mislav Milicevic edited this page Nov 4, 2019 · 13 revisions

Having covered Speedment application setup and creating a REST API using Spring Boot in previous tutorials, this tutorial will describe how to add JWT integration allowing query results to be affected by the contents of JSON Web Tokens supplied from the client.

Added Dependencies for JWTs and Authentication

Starting from the the last tutorial, a REST API to the Sakila movie database, we will use spring-boot-starter-security to add authentication and standard libraries for handling JSON Web Tokens. The following is needed in the pom.xml file to achieve that.

        <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>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jdk8</artifactId>
        </dependency>

This gives our application access to all algorithms needed to handle the logic of this tutorial. What we need to add is some pieces of code putting it all together and we will see how nicely the solution integrated with Speedment.

Configuring the Authentication

We need a class counting the specific logic for authentication of our application as follows.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JWTAuthenticationFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("needforspeed")
                .password("pwd")
                .authorities("PG-13", "PG-15");
    }
}

We add filters to allow access to the /login path for all, add a login filter for said path and a JWT checking filter for all others. The logic is that an original request for /login is always allowed and will be used to return a JWT header which the client will send back in subsequent requests, which can then be authenticated by checking the signature of the JWT.

In configure we create a user which i authorized to see films that are rated precisely PG-13 or PG-15 (but is blocked from too childish content). These authorities will be added to the JWT when logging in to be used to filter the results of subsequent film lookups.

Token Authentication

The following code creates, signs, parses and validates the JWTs used.

class TokenAuthenticator {
    private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000;  // 7 days
    private static final String AUTH_HEADER_STRING = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer";
    private static final String SECRET = "OurSecret";
    private static final String PG_CLAIM_LABEL = "PG";

    static void addAuthentication(HttpServletResponse res, String userName, Collection<String> autorizations) {

        List<String> pgRatings = autorizations.stream()
                .filter(s -> s.startsWith(PG_CLAIM_LABEL))
                .collect(Collectors.toList());

        String jwt = Jwts.builder()
                .setSubject(userName)
                .claim(PG_CLAIM_LABEL, pgRatings)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();

        res.addHeader(AUTH_HEADER_STRING, TOKEN_PREFIX + " " + jwt);
    }

    static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(AUTH_HEADER_STRING);
        if (token != null) {
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""));

            Claims body = claims.getBody();
            String userName = body.getSubject();

            final Collection<String> pgStrings = (Collection<String>) body.get(PG_CLAIM_LABEL);
            final Set<String> credentials = unmodifiableSet(new HashSet<>(pgStrings));
            return userName != null ?
                    new UsernamePasswordAuthenticationToken(userName, credentials, emptyList()) :
                    null;
        }
        return null;
    }
}

The method addAuthentication(...) creates and signs a JWT that has a subject containing the username and claims of PG rating allowed for the user in question and the method getAuthentication(HttpServletRequest request) parses the JWT of the headers of an incoming request

Filters to Connect the Logic to the Request Handlers

The following filter parses the JWT from the headers of an incoming request:

public class JWTAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = TokenAuthenticator.getAuthentication((HttpServletRequest)request);

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);

        filterChain.doFilter(request,response);
    }
}

... while the login is hooked into the request handling by the following filter.

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

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

        AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);

        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword(),
                        Collections.emptyList()
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        List<String> pgRating = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        TokenAuthenticator.addAuthentication(res, auth.getName(), pgRating);
    }
}

The referred class AccountCredentials is just a POJO with username and password to make POST body parsing simple.

public class AccountCredentials {
    private String username;
    private String password;

    // getters and setters...
}

Connect the JWT to Speedment Filters

Now we have all the application logic to allow for login and authenticated requests. To filter the Sakila movie database lookups from the last tutorial all the application logic has to do is to look for the PG rating credentials of the logged in user.

The root request for all films can be changed from

    @GetMapping("")
    List<Film> getFilms() {
        return films.stream().collect(toList());
    }

to

    @GetMapping("")
    List<Film> getFilms() {
        return getFilmStream().collect(toList());
    }

    private Set<String> getCredentials() {
        return (Set<String>) SecurityContextHolder.getContext().getAuthentication().getCredentials();
    }

    private Stream<Film> getFilmStream() {
        return films.stream().filter(Film.RATING.in(getCredentials()));
    }

where instead of operating on the raw stream, we use a filtered stream of films with matching PG rating.

For the other more complicated requests, all we need to do is filter the stream of films by a statement as follows

      .filter(Film.RATING.in(credentials))

The return statement of getting films by actor name thus changes from

        return actorId == null ? emptyList() : filmActors.stream()
                .filter(FilmActor.ACTOR_ID.equal(actorId))
                .map(films.finderBy(FilmActor.FILM_ID))
                .collect(toList());

to

        Set<String> credentials = getCredentials();
        return actorId == null ? emptyList() : filmActors.stream()
                .filter(FilmActor.ACTOR_ID.equal(actorId))
                .map(films.finderBy(FilmActor.FILM_ID))
                .filter(Film.RATING.in(credentials))
                .collect(toList());

Using the Application

To test run the JWT functionality a tool such as Postman is highly valuable. In the following, we will show how to log in and post requests using the tool to inspect and set JWTs in the same way a browser would do.

First, we log in using a POST method as follows:

and get a JWT in response in the Authorization header. The header value starts with the string "Bearer" followed by the actual JWT returned by our server.

The token can be decoded for example at jwt.io and we find that the token is indeed signed using the secret of our application and contains the expected username and PG ratings.

Adding the given JWT to subsequent requests, we will be allowed to GET films with matching PG rating. Requests with missing, malformed or unsigned JWT will be denied.

Clone this wiki locally