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
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
Figure 3 - Creating client invoices-api with authorization enabled
Figure 4 - Creating client invoices-api with authorization enabled
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
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
Next, create two policies: one for the user role and another for the admin role.
Figure 7 - Creating policies of type Role
Then, create a resource for our /api/v1/invoices/*
endpoint.
Figure 9 - Create invoices resource
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
To the create invoices permission we want only the admin to have POST access to create an invoice.
Figure 11 - Create invoices permission
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
Figure 13 - Users with assigned roles
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>
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:
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)