DEV Community

irshad sheikh
irshad sheikh

Posted on • Originally published at initgrep.com on

Spring Security Oauth2- JWT Authentication in a resource server

Oauth2 is an industry-standard protocol for authorization.

As per Oauth2 specification(RFC-6749) —

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.

The following diagram illustrates the working of an Oauth2 authentication request with an Authorization Code Grant flow.

There are four parties involved —

  • Client is a third party Application that wants access to the protected resource from a resource server.
  • Authentication Server is the server which helps to authenticate a Resource Owner.
  • Resource Owner is the user that owns the protected resource.
  • Resource Server is the server which serves the protected resources owned by Resource Owner.

Oauth2 -Auth code grant diagram.drawio.svg

  1. First of all, the client sends an authorization request to Resource Owner so that on behalf of the Resource Owner, it can access the protected resource(s).
  2. If the Authorization-code-grant is used, the Authorization code is returned to the client. It means the Resource owner has given access to the client for protected resources.
  3. The client sends this Authorization code to the Authentication Server, which in return provides an Authentication token — typically a JWT token.
  4. Once the client has the authentication token, It use it to access the protected resources from a resource server. The token expires after a set timeout.

In this post, we will focus on the 4th step i.e. How a Resource Server validates a JWT token provided by any third party client.

First let us understand, what is JWT and what API’s are provided by spring security to implement Jwt Authentication.

What is JWT?

👉🏼 Checkout the complete introduction at jwt.io 😜

Spring Security API for JWT Authentication

The below diagram provides a thorough overview of Spring security API Specs for JWT Authentication.

Spring-security-Oauth2.svg

  • When a client submits a request along with bearer token. It is passed through the security filter chain. The BearerTokenAuthenticationFilter creates a BearerTokenAuthenticationToken of the type Authentication.

  • Next, The AuthenticationManagerResolver resolves the AuthenticationManager which in turn selects the specific AuthenticationProvider.

  • The BearerTokenAuthenticationToken is passed to AuthenticationProvider by ProviderManager( the default Implementation of AuthenticationManager)

  • For JWT authentication, JwtAuthenticationProvider is selected. It decodes, verifies and validates the Jwt using JwtDecoder.

  • If the authentication succeeds, the Authentication is set on the SecurityContextHolder.

  • If the Authentication fails, SecurityContextHolder is cleared.

Finally, Let move ahead with implementing the JWT Authentication.

JWT Authentication in Spring Security

In order to implement it, we would require the following components —

  • Authentication server - we will use Keycloak. It supports Oauth2.0.
  • Resource Server - We will create one using a spring-boot application.
  • Client - We can use Postman API client as the client.
  • User - we will setup one user in Keycloak server.

Authentication server via Keycloak

While you are at it, here are few things, you would require once the Keycloak server is setup.

Make sure to replace authserver.com with valid domain. Also make sure to provide the value value for realm

Once you have the Keycloak server ready — Let’s go ahead and create a resource server.

Resource Server

The resource server will be the simplest one and will contain only one secure rest API.

Dependencies:

  • spring-security-oauth2-resource-server **— Most of the resource server support is collected here.
  • spring-security-oauth2-jose — provides support for decoding and verifying JWT.
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Enter fullscreen mode Exit fullscreen mode

API Endpoint

GET /api/v1/users

@RestController
@RequestMapping("/api/v1")
public class UserController {

    @GetMapping("/users")
    public List<User> getUsers(){
        return Arrays.asList(
                new User("john doe", 100),
                new User("jane doe",300)
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

Since we have Spring security in the class path, every route will be private.

Setup JWT issuer URL

This is minimal setup required to implement the JWT authentication. The issuer-url provided is used by Resource Server to discover public keys of authorization server and validate the token. It is also the same URL present in iss claim.

Spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-url: http://localhost:8080/auth/realms/{realm}

Enter fullscreen mode Exit fullscreen mode

When the Resource server is starts up , it will automatically configure itself to validate JWT encoded Bearer Token. It achieves this querying the Authorization Server metadata endpoint for jwks_url property. This provides access to supported algorithm and its valid public keys.

The Only drawback to this setup is that it would fail if the Authentication server is not already up. To void startup failure, we would need to add the jwk-set-uri

Spring:
    security:
        oauth2:
            resourceserver:
                jwt:
                    issuer-url: http://localhost:8080/auth/realms/dev
                    jwk-set-uri: http://localhost:8080/auth/realms/{realm}/protocol/openid-connect/certs

Enter fullscreen mode Exit fullscreen mode

Now, the Resource Server will not ping the authorization server at startup. However, issuer-uri is still kept to validate the JWT iss claim on incoming token.

Spring Security provides extension points to override or customize the default behavior of the implementation. We will look into customizing some of the default features.

…But before that, Let’s test the default implementation.

Time to Test the Implementation 💎

If you recall, the resource server contains one endpoint with path /api/v1/users. If we call it without providing an authentication token, it will return 401 - Unauthorized status. That is due absence of authorization token.

Let’s see how we can can use authorization code grant to fetch a token from the Keycloak server and use it to access the API provided by the resource server.

Step - 1: Request OAuth Authorization Code

At this point, we would need a client to request the Authorization code. However to make it easier to test, we can run the following URL in the browser. It should redirect you to the login page and you will have to provide the credentials of the user.

http://authserver.com/auth/realms/{realm}/protocol/openid-connect/auth
?client_id=your-client-id&response_type=code&state=app-state

Enter fullscreen mode Exit fullscreen mode

After the successful completion, it will redirect you to the redirect-url with the values similar to below.

http://{redirect-url}/?state=appstate
&session_state=d7c5d4de-c883-494a-a2a2-e5108062830c
&code=f5935f66-88e0-4085-80aa-000b2a6b2b51.d7c5d4de-c883-494a-a2a2-e5108062830c.bf89a5ff-5703-42d6-9534-ca59f667f81f

Enter fullscreen mode Exit fullscreen mode

We would require code to fetch the actual token.

Step - 2: Fetch the Authentication Token

curl -L -X POST "http://localhost:8080/auth/realms/dev/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Cookie: JSESSIONID=E8D36F0DBCBF7E33130B9125F8795CAC.9b51ecd0cc5c; JSESSIONID=8E34666DECDB395B1754FD08C5B385F2" \
--data-urlencode "client_id=client-id-value" \
--data-urlencode "client_secret=client-secret-uuid" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=92236b97-c48f-4827-ae80-80a46e39a0f2.d7c5d4de-c883-494a-a2a2-e5108062830c.bf89a5ff-5703-42d6-9534-ca59f667f81f" \
--data-urlencode "redirect-uri=http://localhost:8085"

Enter fullscreen mode Exit fullscreen mode
  • client-id, client-secret can be fetched from the client credentials in Keycloak server.
  • grant_type = authorization_code describes the grant type used.
  • code fetched in the Step-1.
  • redirect-url should be same as configured for client in Keycloak server.

The response returned would look similar to the below example:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0S1ZQNGVhRVdjdno4ZjRndXlPd05XTE9GWFEzYWo0b1I0eWx0dkFSZldFIn0.eyJleHAiOjE2MzI5MzMzOTksImlhdCI6MTYzMjkzMzA5OSwiYXV0aF90aW1lIjoxNjMyOTMzMDgyLCJqdGkiOiIzZTk1NGRkMi0zMjhhLTQ3NzItYWQ2NS0xOWQ3NGM2MGZjZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGV2IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImMzNjFkMGVjLWM1MWItNGRmNy05MTA1LTVhOWUyZGViMmRjOCIsInR5cCI6IkJlYXJlciIsImF6cCI6InJlc291cmNlc2VydmVyIiwic2Vzc2lvbl9zdGF0ZSI6ImMyYWRiODIyLWQwY2ItNDY3MC1iYmNjLWU3NWJlOTkyYzg5OSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1kZXYiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVzb3VyY2VzZXJ2ZXIiOnsicm9sZXMiOlsiVVNFUiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJ1c2VyX25hbWUiOiJ1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciJ9.A66uqbRwsUL36GSGozZ7FC3x-M4SCYYLaABMdps-XneseP1saIjsTbHO2QrYq2HbD9jl6nKTYxJHjMdbsRJyY3VtM2mf1D8W24-u8y8qmGf1YNbtFfSTZyrUmwiACEv17onAT8wKgR0C4sdbVFETpRY12f2qQb0mM4ZkT9QQ5DYPBu6dnwyBVXLYJzn8kfmp7JB0OR6LsBTTtyh03t_xiRwb1nSALbUmwq7iUk9lTFEUuUZ182p05q3TKxy9b_kxrCh91EYoYWUdBEhRM4yHjrvN99T-MFpRVaCadyn2YibFbCeZHpsqUmgi-ghR3I70U70HGsL22FEAE4N9X5y_pg",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzZGU1NjBjZi1kMDZlLTRiZmItODY2Yi1mNzJhYjk0YjA0NGMifQ.eyJleHAiOjE2MzI5MzQ4OTksImlhdCI6MTYzMjkzMzA5OSwianRpIjoiODQ5OWNmY2QtZjY1Yy00YzdhLThhNDctMzdhNjg4ZGZjMjU0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2RldiIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9kZXYiLCJzdWIiOiJjMzYxZDBlYy1jNTFiLTRkZjctOTEwNS01YTllMmRlYjJkYzgiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoicmVzb3VyY2VzZXJ2ZXIiLCJzZXNzaW9uX3N0YXRlIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5In0.747XKhyNqZCDEzSfLV4K96sgAW0daN1C1ROUr5L_s_E",
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "c2adb822-d0cb-4670-bbcc-e75be992c899",
    "scope": "email profile"
}

Enter fullscreen mode Exit fullscreen mode

Step - 3: Run the API with

We will use the access_token value from previous response as the bearer token to run the private API — (api/v1/users) provided by resource server.

curl -L -X GET "http://localhost:8090/api/v1/users" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0S1ZQNGVhRVdjdno4ZjRndXlPd05XTE9GWFEzYWo0b1I0eWx0dkFSZldFIn0.eyJleHAiOjE2MzI5MzMzOTksImlhdCI6MTYzMjkzMzA5OSwiYXV0aF90aW1lIjoxNjMyOTMzMDgyLCJqdGkiOiIzZTk1NGRkMi0zMjhhLTQ3NzItYWQ2NS0xOWQ3NGM2MGZjZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGV2IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImMzNjFkMGVjLWM1MWItNGRmNy05MTA1LTVhOWUyZGViMmRjOCIsInR5cCI6IkJlYXJlciIsImF6cCI6InJlc291cmNlc2VydmVyIiwic2Vzc2lvbl9zdGF0ZSI6ImMyYWRiODIyLWQwY2ItNDY3MC1iYmNjLWU3NWJlOTkyYzg5OSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1kZXYiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVzb3VyY2VzZXJ2ZXIiOnsicm9sZXMiOlsiVVNFUiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJ1c2VyX25hbWUiOiJ1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciJ9.A66uqbRwsUL36GSGozZ7FC3x-M4SCYYLaABMdps-XneseP1saIjsTbHO2QrYq2HbD9jl6nKTYxJHjMdbsRJyY3VtM2mf1D8W24-u8y8qmGf1YNbtFfSTZyrUmwiACEv17onAT8wKgR0C4sdbVFETpRY12f2qQb0mM4ZkT9QQ5DYPBu6dnwyBVXLYJzn8kfmp7JB0OR6LsBTTtyh03t_xiRwb1nSALbUmwq7iUk9lTFEUuUZ182p05q3TKxy9b_kxrCh91EYoYWUdBEhRM4yHjrvN99T-MFpRVaCadyn2YibFbCeZHpsqUmgi-ghR3I70U70HGsL22FEAE4N9X5y_pg" \
-H "Cookie: JSESSIONID=8E34666DECDB395B1754FD08C5B385F2"

Enter fullscreen mode Exit fullscreen mode

Response:

[
    {
        "name": "John Doe",
        "age": 100
    },
    {
        "name": "Jane Doe",
        "age": 300
    }
]

Enter fullscreen mode Exit fullscreen mode

Next steps in this implementation are to customize the default such as

  • Added a different timeout value to avoid any issues in distributed systems.
  • Configure GrantedAuthority mapping
  • .. and much more.

I have written a comprehensive tutorial about it.
You can check it out here

Top comments (0)