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.