In parts 1 through 3 of this series, we built a Spring API that can issue a JWT when a user successfully authenticates. In this blog post, we will add the capability to verify the JWT presented by the client for subsequent requests.
These are the blog posts in this series:
- Part 1 - Discussion of JWT and implementation
- Part 2 - A Spring User Profiles API
- Part 3 - Issuing a token from the server
- Part 4 - Verifying the token sent back by the client
- Part 5 - Securing the front end
We will start by configuring Spring security with a filter to capture the JWT passed by the client in the Authorization
header. We will wire up this filter to go before the UsernamePasswordAuthenticationFilter
provided by Spring security.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthFilter jwtAuthFilter;
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/login")
.permitAll()
.antMatchers("/**/*")
.hasAuthority("ROLE_USER")
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// ...
}
The filter captures the Authorization
header and creates a JwtAuthToken
and sets that as the current authentication for the request.
@Component
public class JwtAuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String authorization = servletRequest.getHeader("Authorization");
if (authorization != null) {
JwtAuthToken token = new JwtAuthToken(authorization.replaceAll("Bearer ", ""));
SecurityContextHolder.getContext().setAuthentication(token);
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
The JwtAuthToken
is simply a conduit to carry the token and although it implements the Authentication
interface, it does not do much.
public class JwtAuthToken implements Authentication {
private final String token;
public JwtAuthToken(String token) {
this.token = token;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public Object getDetails() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
@Override
public boolean isAuthenticated() {
return false;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return null;
}
}
At this point, we need to tell Spring Security how to verify the tokens. This can be done by providing a custom AuthenticationProvider
. This can be done by overriding the configure
method.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
// ...
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jwtAuthenticationProvider);
}
// ...
}
The JwtAuthenticationProvider
receives the Authentication
instance set on the SecurityContext
, which in our case is the JwtAuthToken
we set using the JwtAuthFilter
. This token is then verified using the JwtService
. If the token is valid, we return a JwtAuthenticatedProfile
or throw an exception if it is invalid.
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtService jwtService;
@SuppressWarnings("unused")
public JwtAuthenticationProvider() {
this(null);
}
@Autowired
public JwtAuthenticationProvider(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
Optional<MinimalProfile> possibleProfile = jwtService.verify((String) authentication.getCredentials());
return new JwtAuthenticatedProfile(possibleProfile.get());
} catch (Exception e) {
throw new JwtAuthenticationException("Failed to verify token", e);
}
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthToken.class.equals(authentication);
}
}
JwtAuthenticatedProfile
is another implementation of Authentication
that wraps the user’s profile information:
public class JwtAuthenticatedProfile implements Authentication {
private final MinimalProfile minimalProfile;
public JwtAuthenticatedProfile(MinimalProfile minimalProfile) {
this.minimalProfile = minimalProfile;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return minimalProfile.getUsername();
}
}
Next, we implement the verify functionality in JwtService
.
public Optional<MinimalProfile> verify(String token) throws IOException, URISyntaxException {
byte[] secretKey = secretKeyProvider.getKey();
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return profileService.minimal(claims.getBody().getSubject().toString());
}
When we generated the JWT, we had set the username as the JWT subject.
The last thing to do is to ensure that we handle exceptions that occur during token verification gracefully. Since the token verification is happening outside controllers, we won’t be able to leverage ControllerAdvice
to handle exceptions. This is where Spring Security’s AuthenticationEntryPoint
comes in to play. We will configure a custom AuthenticationEntryPoint
as follows:
http.authorizeRequests()
.antMatchers("/login", "/bower_components/**/*", "/app/**/*", "/index.html")
.permitAll()
.antMatchers("/**/*")
.hasAuthority("ROLE_USER")
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(jwtAuthEndPoint);
Our entry point, sets the HTTP status to 403
, and sets the response body to a JSON showing the error.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.setStatus(SC_FORBIDDEN);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
String message;
if(e.getCause() != null) {
message = e.getCause().getMessage();
} else {
message = e.getMessage();
}
byte[] body = new ObjectMapper()
.writeValueAsBytes(Collections.singletonMap("error", message));
httpServletResponse.getOutputStream().write(body);
}
}
With this configuration in place, we can request a token and make a subsequent request with the received JWT.
$ curl -i -X POST "http://localhost:8080/login" -d '{"username":"greenrabbit948", "password":"celeste"}' --header "Content-Type: application/json"
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJncmVlbnJhYmJpdDk0OCIsImV4cCI6MTQ2ODE0MDg1MiwiaXNzIjoiaW4uc2RxYWxpLmp3dCJ9.t9pqrOmYfaVkzuAQgo4D4VbN2PibQuHPuPA6RKYU-keTzbFAX58l77hQTc4Cq28HpjFOeiDvNpNEgilNHFOfVA
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 10 Jul 2016 06:54:12 GMT
{"username":"greenrabbit948","name":{"title":"miss","first":"dionaura","last":"rodrigues"},"thumbnail":"https://randomuser.me/api/portraits/thumb/women/78.jpg"}
$ curl -s "http://localhost:8080/profile/details/yellowfrog347" --header "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJncmVlbnJhYmJpdDk0OCIsImV4cCI6MTQ2ODE0MDg1MiwiaXNzIjoiaW4uc2RxYWxpLmp3dCJ9.t9pqrOmYfaVkzuAQgo4D4VbN2PibQuHPuPA6RKYU-keTzbFAX58l77hQTc4Cq28HpjFOeiDvNpNEgilNHFOfVA" | jq .
{
"picture": {
"large": "https://randomuser.me/api/portraits/women/71.jpg",
"medium": "https://randomuser.me/api/portraits/med/women/71.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/women/71.jpg"
},
"name": {
"title": "ms",
"first": "sofia",
"last": "hansen"
},
"email": "[email protected]",
"username": "yellowfrog347"
}
If we were to make a request with an invalid JWT, we will receive an error:
$ curl -s "http://localhost:8080/profile/details/yellowfrog347" --header "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJncmVlbnJhYmJpdDk0OCIsImV4cCI6MTQ2ODE0MDg1MiwiaXNzIjoiaW4uc2RxYWxpLmp3dCJ9.t9pqrOmYfaVkzuAQgo4D4VbN2PibQuHPuPA6RKYU-keTzbFAX58l77hQTc4Cq28HpjFOeiDvNpNEgilNHFOfVAAAAAA" | jq .
{
"error": "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted."
}
In the next blog post, the fifth is this series, we will move on to building the front end with Angular JS and managing authentication from the front end. The source code for this example for the progress made from part 1 through part 4 is available on GitHub.