Bad, good or best practice?
Requirement: Secure an API, MyService, developed with Spring boot and RestController. The client provides an authentication JWT
Details:
- The JWT is provided by a front end via a standard flow
- Permissions on keycloak are defined via granularly configured permissions, for example GET:STATUS is an authorization scope to read the states of a resource.
Keycloak:
- Added two clients to keycloak, one for the front end application (standard flow) called client_fe and one called client_service for the states service.
- Permissions configurations for client_service:
– n. 3 AUTHORIZATION_SCOPE: DELETE:STATUS, GET:STATUS e POST:STATUS
– n. 1 RESOURCE called status-check-resource with URI=/users/status/check and scopes: DELETE:STATUS, GET:STATUS e POST:STATUS
– n. 2 POLICY based on role:
— only_configurator based on role configurator
— only_doctor based on role doctor
– n. 3 PERMISSIONS based on scope
— delete-status-permission, DELETE:STATUS, only_configurator and only_doctor
— get-status-permission, GET:STATUS, only_doctor
— post-status-permission, POST:STATUS, only_configurator
Spring Boot (MyService):
pom.xml
<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>
application.yml
spring.security.oauth2.resourceserver.jwt.jwk-set-uri = http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
Parts of java controller class StatusController (RestController, RequestMapping(“/users”))
@PreAuthorize("hasAuthority('SCOPE_GET:STATUS')")
@GetMapping("/status/check"){...}
@PreAuthorize("hasAuthority('SCOPE_DELETE:STATUS')")
@DeleteMapping("/status/check"){...}
@PreAuthorize("hasAuthority('SCOPE_POST:STATUS')")
@PostMapping("/status/check"){...}
Spring security layer configuration
...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakScopeConverter());
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(
oauth2 -> oauth2.jwt(
jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)
)
);
return http.build();
}
KeycloakScopeConverter, a java class that, given the incoming JWT (the one provided by the front end), obtains an RPT token and extracts the authorization scopes from this.
makes an http POST with
url: http://localhost:8080/realms/myrealm/protocol/openid-connect/token
grant_type: urn:ietf:params:oauth:grant-type:uma-ticket
audience: client_service
header: "Authorization", " Bearer " + jwt.getTokenValue()) (jwt fornito dal FE)
The scopes are extracted from the array permissions:
ArrayList<String> _scopes = new ArrayList<>();
for (Map<String, Object> permission : permissions) {
ArrayList<String> scopes = (ArrayList<String>) permission.get("scopes");
if (scopes != null) {
for (String scope : scopes) {
_scopes.add(scope);
}
}
}
Collection<GrantedAuthority> returnValue = _scopes
.stream().map(scope -> "SCOPE_" + scope)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
In this way a control using the PreAuthorize(“hasAuthority(”…"))
The question is: Is this a bad, good or best practice?
Over to you.