Tutorial: Speedment Stream Filters Using JWT Data
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.
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.
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.
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
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...
}
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());
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.
Speedment is available under the Apache 2 license.
Want to learn more about the enterprise version? Visit www.speedment.com!