Securing a Spring RESTful Web Service with JWT
Java, Maven, Spring Boot, REST, JWT, Security
JSON Web Tokens (JWT) are an open, industry standard method for representing claims securely between two parties.
Introduction
In a previous post we developed a REST
Web Service with Spring.
In the post I talked about how you can implement a REST Web Service with Spring. We also talked about the various
HTTP methods and how to map them to Spring’s RequestMapping
composite annotations.
In the post however, we did not talk about how to secure a Web Service. Security is an important aspect of
building a Web Service and more often than not, it is overlooked.
In this post we are going to secure the web service using JWT with Spring Security.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. To get started with this post, get the source for the previous post as zip|tar.gz and extract the contents of the archive.
Project Structure
At the end of this guide our folder structure will look similar to the following:
.
|__src/
| |__main/
| | |__java/
| | | |__com/
| | | | |__juliuskrah/
| | | | | |__Application.java
| | | | | |__Resource.java
| | | | | |__ResourceService.java
| | | | | |__security/
| | | | | | |__JWTAccessDeniedHandler.java
| | | | | | |__JWTAuthenticationEntryPoint.java
| | | | | | |__SecurityConfig.java
| | | | | | |__jwt/
| | | | | | | |__JWTAuthenticationFilter.java
| | | | | | | |__JWTConfigurer.java
| | | | | | | |__JWTFilter.java
| | | | | | | |__TokenProvider.java
|__pom.xml
Prerequisites
To follow along this guide, your development system should have the following setup:
JWT: An Introduction
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC+ algorithm) or a public/private key pair using RSA.
JSON Web Tokens consist of three parts separated by dots (.
), which are:
- Header
- Payload
- Signature
Therefore, a JWT typically looks like the following.
xxxxx.yyyyy.zzzzz
You can find a breakdown of the different parts on the Official Website.
Authentication Schemes
Security in a Web Application usually boils down Authentication and Authorization. There are several
shemes
involved when doing Authentication and Authorization. Some of the common ones are listed below:
- Basic (RFC 7617, base64-encoded credentials.),
- Bearer (RFC 6750, bearer tokens to access OAuth 2.0-protected resources),
- Digest (RFC 7616, only md5 hashing is supported in Firefox),
- HOBA (RFC 7486 (draft), HTTP Origin-Bound Authentication, digital-signature-based),
- Mutual (draft-ietf-httpauth-mutual),
- AWS4-HMAC-SHA256 (AWS docs).
IANA maintains a list of authentication schemes here.
We will use the Bearer
authentication scheme in this post.
Add Dependencies for Security
We will add the dependencies for JWT
and Spring Security
:
file:
pom.xml
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
Since we are leveraging the power of Spring-Boot, the above dependency to Spring Security registers the Spring
Security Filter Chain and sets up the Basic
Authentication Scheme.
Open a terminal and run the project:
> mvnw clean spring-boot:run
Open another terminal and run:
> curl -i -H "Accept: application/json" http://localhost:8080/api/v1.0/resources
HTTP/1.1 401
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
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
WWW-Authenticate: Basic realm="Spring"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 07 Nov 2017 10:10:16 GMT
{
"timestamp":"2017-11-07T10:10:16.665+0000",
"status":401,
"error":"Unauthorized",
"message":"Full authentication is required to access this resource",
"path":"/api/v1.0/resources"
}
As you can see, you got an Unauthorized
when you tried to access a protected resource. Let’s create a password
and username and try again:
file:
src/main/resources/application.properties
security.user.name=julius
security.user.password=secret
> curl -i -H "Accept: application/json" http://localhost:8080/api/v1.0/resources -ujulius:secret
HTTP/1.1 200
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
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 07 Nov 2017 10:21:47 GMT
[
{
"id":1,
"description":"Resource One",
"createdTime":"2017-11-07T10:21:23.049",
"modifiedTime":null
},
{
"id":2,
"description":"Resource Two",
"createdTime":"2017-11-07T10:21:23.049",
"modifiedTime":null
}
...
]
The -u
switch is to inform curl we intend to authenticate with a username:password combination. You can also request with the Base64
encoding:
> curl -i -H "Accept: application/json" -H "Authorization: Basic anVsaXVzOnNlY3JldA==" http://localhost:8080/api/v1.0/resources
JWT and Bearer Authentication
Now that we have seen the Basic
scheme in action, let us now switch to the Bearer
scheme using JWT.
Create the Spring Security WebSecurityConfigurerAdapter
class:
file:
src/main/java/com/juliuskrah/security/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder auth;
private final TokenProvider tokenProvider;
...
public SecurityConfig(AuthenticationManagerBuilder auth, TokenProvider tokenProvider) {
//
}
@PostConstruct
public void init() {
try {
auth
.inMemoryAuthentication()
.withUser("julius").password("secret").roles("USER")
.and()
.withUser("admin").password("admin").roles("USER", "ADMIN");
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new JWTAuthenticationFilter(
"/api/authenticate", this.tokenProvider, authenticationManager()),
UsernamePasswordAuthenticationFilter.class) // <1>
.exceptionHandling()
.authenticationEntryPoint(new JWTAuthenticationEntryPoint()) // <2>
.accessDeniedHandler(new JWTAccessDeniedHandler()) // <3>
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // <4>
.and()
.csrf()
.disable() // <5>
.authorizeRequests().anyRequest().authenticated() // <6>
.and()
.apply(securityConfigurerAdapter()) // <7>
}
}
- This is to ensure that the Authentication filter is called before the
UsernamePasswordAuthenticationFilter
. Once this is registered first, this will be called before the other authentication filters in the Spring Security Filter Chain. This filter will check for a valid login before proceeding. All login attempts will be a POST request to/api/authenticate/
. - The
AuthenticationEntryPoint
will ensure users are presented with anUnauthorized
401 response. This will override the Spring Security default of redirecting to a login page. Login pages do not make sense for REST services. -
AccessDeniedHandler
for when a user tries to access a resource he is not permitted to access. - We will set session management to
STATELESS
for REST is stateless. - We will disable
CSRF
protection. It is not required in REST. - Make sure all routes require authentication.
- Apply a security configurer to authenticate all requests.
Create a class that takes care of the concern
of enconding and decoding the JWTs:
file:
src/main/java/com/juliuskrah/security/jwt/TokenProvider.java
@Component
public class TokenProvider {
private static final String AUTHORITIES_KEY = "auth";
private String secretKey = "secret";
private long tokenValidityInMilliseconds = 1000 * 86400;
public String createToken(Authentication authentication) {
// Click on link above for implementation
}
public Authentication getAuthentication(String token) {
// Decode token
}
public boolean validateToken(String authToken) {
// Return true if token is valid
}
}
We need a filter class to filter all requests for the Bearer
token:
file:
src/main/java/com/juliuskrah/security/jwt/JWTFilter.java
public class JWTFilter extends GenericFilterBean {
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
// Filter each request for the Bearer token
}
private String resolveToken(HttpServletRequest request) {
// Resolve token
}
}
We put it all together in JWTConfigurer
that will encapsulate each request:
file:
src/main/java/com/juliuskrah/security/jwt/JWTConfigurer.java
public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_TOKEN = "access_token";
private TokenProvider tokenProvider;
public JWTConfigurer(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
With all this done, we need to be able to login to generate the token that will be used for subsequent requests:
file:
src/main/java/com/juliuskrah/security/jwt/JWTAuthenticationFilter.java
public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final TokenProvider tokenProvider;
private final AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(String defaultFilterProcessesUrl, TokenProvider tokenProvider,
AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
this.tokenProvider = tokenProvider;
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// Do authentication
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authentication) throws IOException, ServletException {
// Add Bearer token to header
}
}
We can test what we did:
> curl -X POST -i -H "Accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" http://localhost:8080/api/authenticate -d "username=julius&password=secret"
HTTP/1.1 200
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
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqdWxpdXMiLCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNTEwMTgwMDg4fQ.taul450j0s39mnyzfdd3jurloRUlCowJK6Vd6EmN2tv5y9iiGTfPXyfLBDx0NLOZKUHcbhgYSUe_AoK_DzybGg
Content-Length: 0
Date: Tue, 07 Nov 2017 11:05:14 GMT
Notice the Authorization
header with the Bearer token
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqdWxpdXMiLCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNTEwMTgwMDg4fQ.taul450j0s39mnyzfdd3jurloRUlCowJK6Vd6EmN2tv5y9iiGTfPXyfLBDx0NLOZKUHcbhgYSUe_AoK_DzybGg
We will use this token to access all other resources in our service:
> curl -i -H "Accept: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqdWxpdXMiLCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNTEwMTgwMDg4fQ.taul450j0s39mnyzfdd3jurloRUlCowJK6Vd6EmN2tv5y9iiGTfPXyfLBDx0NLOZKUHcbhgYSUe_AoK_DzybGg" http://localhost:8080/api/v1.0/resources
HTTP/1.1 200
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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 07 Nov 2017 11:34:46 GMT
[
{
"id":1,
"description":"Resource One",
"createdTime":"2017-11-07T11:27:36.175",
"modifiedTime":null
},
{
"id":2,
"description":"Resource Two",
"createdTime":"2017-11-07T11:27:36.175",
"modifiedTime":null
}
...
]
The token can also be passed as a query parameter in the URL:
> curl -i -H "Accept: application/json" http://localhost:8080/api/v1.0/resources?access_token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqdWxpdXMiLCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNTEwMTgwMDg4fQ.taul450j0s39mnyzfdd3jurloRUlCowJK6Vd6EmN2tv5y9iiGTfPXyfLBDx0NLOZKUHcbhgYSUe_AoK_DzybGg
You can play around with it. Change some values and see how it behaves.
That’s all folks.
Conclusion
In this post we learnt how to secure a RESTful web service with JWT and Spring Security.
As usual you can find the full example to this guide in the github repository. Until the next post, keep doing cool things .