Spring Security with JWT: OAuth 2 Resource Server 
Tags: java, spring, security, jwt
Published: 2020-04-15 17:48:46



Since version 5.2, Spring has introduced a new library, [OAuth 2.0 Resource Sever](https://github.com/spring-projects/spring-security/tree/master/oauth2/oauth2-resource-server), handling JWT  so that we no longer need to manually add a Filter to extract claims from JWT token and verify the token.




## What is a Resource server?

Resource server provides protected resources. It communicates with its Authorization server to validate a request to access a protected resource. Typically the endpoints of a resource server are protected based on the Oauth2 scopes and user roles.
Please refer to [this](https://www.oauth.com/oauth2-servers/the-resource-server/) for more details.

## Example Token

```
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
```

When you decode it from [jwt.io](https://jwt.io/), you find that the JWT structure consists of 3 parts: Header, Payload, Signature.  
### Header
It usually contains two fields:
 1. A type of token, **type**, JWT
 2. The signing algorithm, **alg**, HS256

```json
{
  "typ": "JWT",
  "alg": "HS256"
}
```

### Payload
The payload contains a set of [claims](https://tools.ietf.org/html/rfc7519#section-4.1).  e.g. **iss** (issuer), **exp** (expiration time), **sub** (subject) 
```json
{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "scope": [
    "read"
  ],
  "exp": 4740547387,
  "jti": "c8aa2f77-6666-47f7-b56e-424e1c1e18cb",
  "iat": 1586947387
}
```

 And the claim that is going be used to authorise our endpoints is **scope**: **read**.
>According to [this](https://github.com/spring-projects/spring-security/blob/146d9ba0bfa97d748e0e5fbbff3c1c187a6f22ab/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java#L42), Spring OAuth 2 Resource Server, by default, looks for the clam names: **scope** and **scp**, as they are well-known claims for authorisation. If you are going use a custom claim name, you can see the example at the end of this post.


# Example Project
 
We're going to use [Spring Initializr](https://start.spring.io/) to generate Spring Boot project from scratch.

![](https://res.cloudinary.com/toojannarong/image/upload/c_scale,w_896/v1586692846/blog/spring-resource-sever/spring_init.png)


Here is the dependencies inside build.gradle file:
```java
plugins {
  id 'org.springframework.boot' version '2.2.6.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
test {
  useJUnitPlatform()
}
```

As you can see, we use Spring Boot version 2.2.6.RELEASE. The spring-boot-starter-oauth2-resource-server includes spring-security-oauth2-jose version 5.2.5.RELEASE containing nimbus-jose-jwt library to support JWT decoding. 

----
# Controller
We have created 2 endpoints:
 1. "/" endpoint - accepts HTTP GET method and expects HTTP Header with 'Authorization: Bearer (JWT Token)'
 2. "/message" endpoints accepts 2 HTTP methods: GET and POST

```java
import org.springframework.security.core.annotation.AuthenticationPrincipal;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class Controller {  
  
  @GetMapping("/")  
  public String index(@AuthenticationPrincipal Jwt jwt) {  
    return String.format("Hello, %s!", jwt.getSubject());  
  }  
  
  @GetMapping("/message")  
  public String message() {  
    return "secret message";  
  }  
  
  @PostMapping("/message")  
  public String createMessage(@RequestBody String message) {  
    return String.format("Message was created. Content: %s", message);  
  }  
}
```
# Configuration

1. We define the security rules to the /message endpoint. The message endpoint will check if
  * the request has the authority **read** for GET method
  * the request has the authority **write** for POST method

2. We also tell Spring that we are going use  OAuth2 Resource Sever with JSON Web Token (JWT).

3. We disable 
  * Session Management – this will prevent the creation of session cookies
  * HTTP Basic Authentication
  * Default Spring login page
  * CSRF
.
   
```java
import org.springframework.http.HttpMethod;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;  
  
  
@EnableWebSecurity  
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {  
  
  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("SCOPE_read")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("SCOPE_write")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  
}
```


>Note that this configuration file is expressed as [DSL](https://en.wikipedia.org/wiki/Domain-specific_language). Unlike the traditional approach with builder chaining, we can use Java 8 lamda to express the configurations.

# application.yml

```yml
spring:  
  security:  
    oauth2:  
      resourceserver:  
        jwt:  
          jwk-set-uri: https://login.domain.com/xxx/keys # JSON Web Key URI to use to verify the JWT token.
```
# Expected Results
You’ll get HTTP 403 message when you call secured endpoint with an invalid claims. For example, you sent a token with read **scope** but the endpoints expect **write** scope. 


You’ll get HTTP 401 message when the JWT authorisation fails. For example,
 1. the token is not recognised by the issuer.
 2. the token has expired.
 3. the token is invalid structure.
 4. etc.


# Testing 


Request message endpoint using HTTP **GET**. The token contains **read** scope

```
GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
```
```
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
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 16:12:03 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes
```

Request message endpoint using HTTP **POST**. The token contains **read** scope. But it expects **write** scope.

```
POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
```

```
HTTP/1.1 403 
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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 20:12:00 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte
```

--------------------------------------------------

# Custom claim
What if our JWT does not contain the well-known claims(**scope**, **scp**) for authorisation?

We'll use claim name: **roles** as example. 

### Token with claim: **roles**

```
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
```

### Payload
```json
{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "roles": [
    "student"
  ],
  "exp": 4740570123,
  "jti": "379ea761-3e50-4362-8e12-d072346a7be1",
  "iat": 1586970123
}
```

This section is going to illustrate on how to modify a the [Default JWT Converter](https://github.com/spring-projects/spring-security/blob/146d9ba0bfa97d748e0e5fbbff3c1c187a6f22ab/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java#L42).


We'll modify our existing configuration file as follows:

```java
package com.example.resourcesever;  
  
import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  
  
import java.util.Collection;  
  
  
@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  
  
  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  
  
  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("ROLE_student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("ROLE_admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  
  
  private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  
  
  private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}
 ```

From above, first, we tell Spring that we want to use claim name **roles** instead of **scope** or **scp**. 
- Only **student** role will pass the GET method authorisation.
- Only **admin** role will pass the POST method authorisation.

Second, we want to set authority prefix with **ROLE_** instead of **SCOPE_**


We can modify it further...  We can use **hasRole** instead of **hasAuthority**. 

```java
package com.example.resourcesever;  
  
import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  
  
import java.util.Collection;  
  
  
@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  
  
  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  
  
  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasRole("student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasRole("admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  
  
  private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  
  
  private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}
 ```

Notice that I provide only the role name(**admin**, **student**) without the prefix **ROLE_** to the hasRole() method, because the [implementation of hasRole()](https://github.com/spring-projects/spring-security/blob/146d9ba0bfa97d748e0e5fbbff3c1c187a6f22ab/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java#L245) does not expect us to put the prefix **ROLE_**, it will do for us.

# Testing with Custom claim: roles

Request message endpoint using HTTP **GET**. The token contains **student** role

```
GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
```

```
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
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 17:11:01 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes
```

Request message endpoint using HTTP **POST**. The token contains **student** role. But it expects **admin** role.

```
POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
```

```
HTTP/1.1 403 
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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 22:10:05 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte
```

# Conclusion
The OAuth 2 Resource Sever library provide us a minimal configuration. We do not need to write a filter anymore.