DEV Community

Felipe Xavier
Felipe Xavier

Posted on

How to secure a single REST API resource with multiple scopes using Keycloak

How to secure a single REST API resource with multiple scopes using Keycloak

Today, we will discuss authorization and how to secure a single endpoint—a resource—with multiple scopes using Keycloak. Consider a REST API for managing invoices. We will set up several endpoints based on the following HTTP methods:

GET /api/v1/invoices 
GET /api/v1/invoices/{id} 
POST /api/v1/invoices
PUT /api/v1/invoices/{id} 

Our application architecture will look like this:

Figure 1 - Example application architecture

Image description

In this setup, we have two main roles: user and admin. A user can view invoices, while an admin has full access. Initially, we’ll manage a single Keycloak resource /api/v1/invoices/*, and work with scopes and role-based permissions to validate each request. Though we’ll be using Spring in the example, the concepts can be implemented in any other language or client.

First things first, setup a Keycloak server using docker.

docker run --network=host -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -e KC_DB=postgres -e KC_DB_URL=jdbc:postgresql://localhost:5432/invoices -e KC_DB_USERNAME=postgres -e KC_DB_PASSWORD=postgres -e KC_HOSTNAME=localhost quay.io/keycloak/keycloak:25.0.1 start-dev --http-port=8180

This runs Keycloak on port 8180 (version 25.0.1) with a PostgreSQL database on port 5432. After starting Keycloak successfully, create a realm named invoices and a client invoices-api, with authorization enabled.

Figure 2 - Creating realm invoices
Image description

Figure 3 - Creating client invoices-api with authorization enabled
Image description

Figure 4 - Creating client invoices-api with authorization enabled
Image description

With our realm and client in hands we start to create the authorization part. First we create two roles to distinguish a user and an admin.

Figure 5 - Creating roles on invoices-api client
Image description

Now, under Clients > invoices-api > Authorization, we’ll define our scopes. Each scope corresponds to an HTTP method (GET, POST, PUT, PATCH, DELETE). For example, a GET scope allows users to view a resource, while a POST scope allows resource creation.

Figure 6 - Creating scopes as HTTP methods
Image description

Next, create two policies: one for the user role and another for the admin role.

Figure 7 - Creating policies of type Role
Image description

Figure 8 - Created policies
Image description

Then, create a resource for our /api/v1/invoices/* endpoint.

Figure 9 - Create invoices resource
Image description

In the code example that follows, we implement the GET /api/v1/invoices and POST /api/v1/invoices endpoints. To secure these, we’ll create permissions to associate scopes with roles. The list invoices permission corresponds to the GET scope, allowing both user and admin roles to list invoices. Ensure that the decision strategy is set to affirmative, meaning at least one policy must be satisfied.

Figure 10 - View invoices permission
Image description

To the create invoices permission we want only the admin to have POST access to create an invoice.

Figure 11 - Create invoices permission
Image description

For now we’re done for the setup of keycloak authorization. Now lets create two users to test and assign one with role admin and other as user.

Figure 12 - Assign role to user
Image description

Figure 13 - Users with assigned roles
Image description

Now let’s go to the application code. We have a basic Spring 3 application with these dependencies mainly:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-policy-enforcer</artifactId>
    <version>23.0.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

On the application.yaml we need to set the Keycloak url and path to certs to be able to verify and call Keycloak.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:8180/realms/invoices/protocol/openid-connect/certs

We also need to add a policy-enforcer.json file in the resources folder:

{
  "realm": "invoices",
  "auth-server-url": "http://localhost:8180",
  "resource": "invoices-api",
  "http-method-as-scope": true,
  "credentials": {
    "secret": "your-client-secret"
  }
}

The key aspect is that http-method-as-scope is set to true. This parameter instructs the application to use HTTP methods as scopes when communicating with Keycloak. These scopes correspond to the ones we previously created in the authorization tab. Essentially, every request made to the invoices API is also sent to Keycloak, where it verifies whether the authenticated user has the required scope for the HTTP call. Under the hood, Keycloak leverages UMA, a layer built on top of the OAuth protocol, with extended capabilities for resource authorization. You can explore the full documentation and architecture in the official documentation at https://www.keycloak.org/docs/latest/authorization_services/index.html.

Our Spring security configuration will look like this:

@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerSecurityConfiguration {

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    String jwkSetUri;

    @bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);
        return http.build();
    }

    private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() {
        PolicyEnforcerConfig config;

        try {
            config = JsonSerialization.readValue(getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return new ServletPolicyEnforcerFilter(new ConfigurationResolver() {
            @override
            public PolicyEnforcerConfig resolve(HttpRequest request) {
                return config;
            }
        });
    }

    @bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
    }

}

Observe that we don’t need to define any authorization information or roles in our application, that are all delegated to Keycloak. With the previous policy-enforcer.json file when the application starts it will query the Keycloak discovery endpoints to register the rules.

Our controller will look like this:

@RestController
public class InvoicesController {

    @GetMapping("/api/v1/invoices")
    public String listInvoices(@AuthenticationPrincipal Jwt jwt) {
        return "List invoice";
    }

    @GetMapping("/api/v1/invoices/{id}")
    public String getInvoice(@AuthenticationPrincipal Jwt jwt, @PathVariable String id) {
        return "Detail invoice " + id;
    }

    @PostMapping("/api/v1/invoices")
    public String createInvoice(@AuthenticationPrincipal Jwt jwt) {
        return "Creating an invoice";
    }

}

So let’s test our user, he should have access to only view invoices.

export access_token=$(\
curl -X POST http://localhost:8180/realms/invoices/protocol/openid-connect/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=invoices-api&client_secret=' \
-d 'username=user&password=user&grant_type=password' | jq --raw-output '.access_token' \
)

curl http://localhost:8080/api/v1/invoices \
  -H "Authorization: Bearer "$access_token

curl http://localhost:8080/api/v1/invoices \
  -H "Authorization: Bearer "$access_token

Running the requests we can see that the authenticated user had the access granted, now requesting to create an invoice.

curl -X POST http://localhost:8080/api/v1/invoices \
  -H "Authorization: Bearer "$access_token

We receive 403 since we don’t have the scope permission, but signing in with the admin the request was successful.

export access_token=$(\
curl -X POST http://localhost:8180/realms/invoices/protocol/openid-connect/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=invoices-api&client_secret=' \
-d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
)

curl -X POST http://localhost:8080/api/v1/invoices \
  -H "Authorization: Bearer "$access_token

When a request is sent to the invoices-api the internal filters sends a request to the Keycloak to check if the authenticated user in the authorization header has the necessary permissions, the request made to the Keycloak server will look like this:
Image description

Note that the subject_form in the form parameters corresponds to the authorization header, and the grant type is from the UMA flow that Keycloak uses to authorize a resource. The Spring Security and policy enforcement library handles this process for us, but it can be implemented in any other language using the Keycloak API.

Full source code can be found here: https://github.com/FelipeGXavier/keycloak-spring-authorization

Top comments (0)