Skip to content

Conociendo Keycloak - Revisando, refactorizando y actualizando la implementación.

Posted on:August 14, 2023 at 05:42 PM

Conociendo Keycloak - Revisando, refactorizando y actualizando la implementación.

¡Hola de nuevo! Si recuerdan, en mi anterior post nos metimos de lleno con Keycloak en Spring Boot. Sí, lo sé, la implementación fue un poco rápida y, admito, me salté los test unitarios. 😅 Pero, ¿quién no ha hecho eso alguna vez, cierto?

Por lo tanto, en este post vamos a revisar y refactorizar el código para intentar seguir las buenas prácticas de programación y actualizar la implementación de nuestra pequeña app de Keycloak.

Revisando el código.

Primero, echemos un ojo a cómo se ve nuestro proyecto en este momento:

.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── me
    │   │       └── romeralvarez
    │   │           └── springexamplekeycloak
    │   │               ├── SpringExampleKeycloakApplication.java
    │   │               ├── clients
    │   │               │   └── KeycloakClient.java
    │   │               ├── commons
    │   │               │   └── Constants.java
    │   │               ├── configuration
    │   │               │   ├── JwtAuthenticationConverter.java
    │   │               │   └── SecurityConfiguration.java
    │   │               ├── controllers
    │   │               │   ├── KeycloakAuthController.java
    │   │               │   ├── KeycloakController.java
    │   │               │   └── requests
    │   │               │       ├── UserLoginRequest.java
    │   │               │       └── UserRequest.java
    │   │               └── services
    │   │                   ├── KeycloakAuthServiceImpl.java
    │   │                   ├── KeycloakServiceImpl.java
    │   │                   └── interfaces
    │   │                       ├── KeycloakAuthService.java
    │   │                       └── KeycloakService.java
    │   └── resources
    │       ├── application.yaml
    │       ├── banner.txt
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── me
                └── romeralvarez
                    └── springexamplekeycloak
                        └── SpringExampleKeycloakApplicationTests.java

Constants.java

En esta clase se manejan las constantes de la aplicación, sin embargo, tenemos credenciales, URLs y otros datos que no deberían estar en el código, por lo tanto, vamos a moverlos a un archivo de configuración.

En nuestro caso vamos a mover las constantes a un archivo application.yaml en el directorio resources, pero lo ideal es tener un servicio de almacenamiento de secretos como Vault o AWS Secrets Manager.

La decisión que tomaremos, será la de eliminar esta clase y mover las constantes a un archivo application.yaml en el directorio resources.

keycloak:
  url: http://localhost:8080/
  realm-name: spring-boot-realm-dev
  realm-master: master
  admin-cli: admin-cli
  user-console: admin
  user-password: admin
  client-secret: DdWW14rqntQlMvr2AtgCZ1GD7kGSBWsw
  client-id: spring-client-api-rest

KeycloakClient.java

Vamos a revisar el código de la clase KeycloakClient.java:

La clase KeycloakClient actúa como un cliente que facilita la interacción con el servidor Keycloak;

public class KeycloakClient {

  private static final Keycloak keycloakInstance = KeycloakBuilder.builder()
      .serverUrl(KEYCLOAK_URL)
      .realm(REALM_MASTER)
      .clientId(ADMIN_CLI)
      .username(USER_CONSOLE)
      .password(USER_PASSWORD)
      .clientSecret(CLIENT_SECRET)
      .resteasyClient(new ResteasyClientBuilderImpl()
          .connectionPoolSize(10).build())
      .build();

  public static RealmResource getRealmResource() {
    return keycloakInstance.realm(REALM_NAME);
  }

  public static UsersResource getUsersResource() {
    RealmResource realmResource = getRealmResource();
    return realmResource.users();
  }

  public static KeycloakBuilder createKeycloakBuilder(String username, String password) {
    return KeycloakBuilder.builder()
        .realm(REALM_NAME)
        .serverUrl(KEYCLOAK_URL)
        .clientId(CLIENT_ID)
        .clientSecret(CLIENT_SECRET)
        .username(username)
        .password(password);
  }

  public static AccessTokenResponse getAccessTokenForUser(String username, String password) {
    return createKeycloakBuilder(username, password)
        .build()
        .tokenManager()
        .getAccessToken();
  }

}
@Component
public class KeycloakClient {

   @Value("${keycloak.url}")
  private String keycloakUrl;

  @Value("${keycloak.realm-name}")
  private String realmName;

  @Value("${keycloak.realm-master}")
  private String realmMaster;

  @Value("${keycloak.admin-cli}")
  private String adminCli;

  @Value("${keycloak.user-console}")
  private String userConsole;

  @Value("${keycloak.user-password}")
  private String userPassword;

  @Value("${keycloak.client-secret}")
  private String clientSecret;

  @Value("${keycloak.client-id}")
  private String clientId;

  private Keycloak keycloakInstance;

  @PostConstruct
  public void init() {
    this.keycloakInstance = createKeycloakInstance();
  }

  protected Keycloak createKeycloakInstance() {
    return KeycloakBuilder.builder()
        .serverUrl(keycloakUrl)
        .realm(realmMaster)
        .clientId(adminCli)
        .username(userConsole)
        .password(userPassword)
        .clientSecret(clientSecret)
        .resteasyClient(new ResteasyClientBuilderImpl().connectionPoolSize(10).build())
        .build();
  }

  public RealmResource getRealmResource() {
    return keycloakInstance.realm(realmName);
  }

  public UsersResource getUsersResource() {
    RealmResource realmResource = getRealmResource();
    return realmResource.users();
  }

  public KeycloakBuilder createKeycloakBuilder(String username, String password) {
    return KeycloakBuilder.builder()
        .realm(realmName)
        .serverUrl(keycloakUrl)
        .clientId(clientId)
        .clientSecret(clientSecret)
        .username(username)
        .password(password);
  }

  public AccessTokenResponse getAccessTokenForUser(String username, String password) {
    return createKeycloakBuilder(username, password)
        .build()
        .tokenManager()
        .getAccessToken();
  }

}

KeycloakServiceImpl.java

Daremos un vistazo a como estaba anteriormente la clase:

@Service
@Slf4j
public class KeycloakServiceImpl implements KeycloakService {

  @Override
  public List<UserRepresentation> findAllUsers() {
    log.info("Finding all users");
    log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
    return KeycloakClient.getRealmResource().users().list();
  }

  @Override
  public List<UserRepresentation> searchByUsername(String username) {
    return KeycloakClient.getRealmResource().users().searchByUsername(username, true);
  }

  @Override
  public String createUser(@NonNull UserRequest userRequest) {
    log.info("Creating user");
    int status = 0;

    log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
    UsersResource usersResource = KeycloakClient.getUsersResource();
    UserRepresentation userRepresentation = new UserRepresentation();

    log.info("Setting user attributes");
    userRepresentation.setFirstName(userRequest.firstName());
    userRepresentation.setLastName(userRequest.lastName());
    userRepresentation.setEmail(userRequest.email());
    userRepresentation.setUsername(userRequest.username());
    userRepresentation.setEnabled(true);
    userRepresentation.setEmailVerified(true);

    Response response = usersResource.create(userRepresentation);
    status = response.getStatus();

    if (status == 201) {
      log.info("User created");
      String path = response.getLocation().getPath();
      String userId = path.substring(path.lastIndexOf('/') + 1);
      log.info("User id: {}", userId);

      log.info("Setting user password");
      CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
      credentialRepresentation.setTemporary(false);
      credentialRepresentation.setType(OAuth2Constants.PASSWORD);
      credentialRepresentation.setValue(userRequest.password());

      log.info("Resetting user password");
      usersResource.get(userId).resetPassword(credentialRepresentation);

      log.info("Assigning user roles");
      RealmResource realmResource = KeycloakClient.getRealmResource();
      List<RoleRepresentation> roles = null;
      
      if(userRequest.roles() == null || userRequest.roles().isEmpty()){
        roles = List.of(realmResource.roles().get("user").toRepresentation());
      } else {
        roles = realmResource
            .roles()
            .list()
            .stream()
            .filter(role -> userRequest.roles()
                .stream()
                .anyMatch(roleName -> roleName.equalsIgnoreCase(role.getName())))
            .toList();
      }
      realmResource
          .users()
          .get(userId)
          .roles()
          .realmLevel()
          .add(roles);

      return "User created";
    } else if (status == 409) {
      log.error("User already exists");
      return "User already exists";
    } else {
      log.error("Error creating user");
      return "Error creating user";
    }

  }

  @Override
  public void deleteUser(String userId) {
    log.info("Deleting user");
    log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
    KeycloakClient.getUsersResource().get(userId).remove();
  }

  @Override
  public void updateUser(String userId, UserRequest userRepresentation) {
    log.info("Updating user");
    log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
    CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
    credentialRepresentation.setTemporary(false);
    credentialRepresentation.setType(OAuth2Constants.PASSWORD);
    credentialRepresentation.setValue(userRepresentation.password());

    UserRepresentation user = new UserRepresentation();
    user.setFirstName(userRepresentation.firstName());
    user.setLastName(userRepresentation.lastName());
    user.setEmail(userRepresentation.email());
    user.setUsername(userRepresentation.username());
    user.setEnabled(true);
    user.setEmailVerified(true);
    user.setCredentials(Collections.singletonList(credentialRepresentation));

    UserResource userResource = KeycloakClient.getUsersResource().get(userId);
    userResource.update(user);
  }
}

Sin duda a simple vista se puede ver que hay mucho código repetido, métodos muy largos, números mágicos, code smells, etc. Vamos explicar un poco que sucede:

Ahora, que hemos evaluado algunos de los problemas, vamos a refactorizar la clase:

public class KeycloakService {
  private final static int STATUS_CREATED = 201;
  private final static int STATUS_CONFLICT = 409;
  private KeycloakClient keycloakClient;
}

Ahora iremos método a método refactorizando:

findAllUsers()

public List<UserRepresentation> findAllUsers() {
  log.info("Finding all users");
  log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
  return KeycloakClient.getRealmResource().users().list();
}
private void logRealmInfo(){
  log.info("Realm: {}", keycloakClient.getRealmResource().toRepresentation().getRealm());
}
public List<UserRepresentation> findAllUsers() {
  log.info("Finding all users");
  logRealmInfo();
  return keycloakClient.getRealmResource().users().list();
}

createUser(@NonNull UserRequest userRequest)

public String createUser(@NonNull UserRequest userRequest) {
  log.info("Creating user");
  int status = 0;

  log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
  UsersResource usersResource = KeycloakClient.getUsersResource();
  UserRepresentation userRepresentation = new UserRepresentation();

  log.info("Setting user attributes");
  userRepresentation.setFirstName(userRequest.firstName());
  userRepresentation.setLastName(userRequest.lastName());
  userRepresentation.setEmail(userRequest.email());
  userRepresentation.setUsername(userRequest.username());
  userRepresentation.setEnabled(true);
  userRepresentation.setEmailVerified(true);

  Response response = usersResource.create(userRepresentation);
  status = response.getStatus();

  if (status == 201) {
    log.info("User created");
    String path = response.getLocation().getPath();
    String userId = path.substring(path.lastIndexOf('/') + 1);
    log.info("User id: {}", userId);

    log.info("Setting user password");
    CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
    credentialRepresentation.setTemporary(false);
    credentialRepresentation.setType(OAuth2Constants.PASSWORD);
    credentialRepresentation.setValue(userRequest.password());

    log.info("Resetting user password");
    usersResource.get(userId).resetPassword(credentialRepresentation);

    log.info("Assigning user roles");
    RealmResource realmResource = KeycloakClient.getRealmResource();
    List<RoleRepresentation> roles = null;
    
    if(userRequest.roles() == null || userRequest.roles().isEmpty()){
      roles = List.of(realmResource.roles().get("user").toRepresentation());
    } else {
      roles = realmResource
          .roles()
          .list()
          .stream()
          .filter(role -> userRequest.roles()
              .stream()
              .anyMatch(roleName -> roleName.equalsIgnoreCase(role.getName())))
          .toList();
    }
    realmResource
        .users()
        .get(userId)
        .roles()
        .realmLevel()
        .add(roles);

    return "User created";
  } else if (status == 409) {
    log.error("User already exists");
    return "User already exists";
  } else {
    log.error("Error creating user");
    return "Error creating user";
  }

}

Como podemos ver, este método es muy largo y hace muchas cosas, vamos a analizar un poco que hemos hecho para que este código sea más legible:

public String createUser(@NonNull UserRequest userRequest) {
  log.info("Creating user");
  logRealmInfo();
  Response response = createUserInKeycloak(userRequest);

  if (response.getStatus() == STATUS_CREATED) {
    handleUserCreated(response, userRequest);
    return "User created";
  } else if (response.getStatus() == STATUS_CONFLICT) {
    log.error("User already exists");
    return "User already exists";
  } else {
    log.error("Error creating user");
    return "Error creating user";
  }

}
  1. Extracción de la lógica de creación de usuarios:
    • Antes: La lógica para crear un usuario en Keycloak estaba incrustada en el método.
    • Después: Se ha definido el método privado createUserInKeycloak(UserRequest userRequest) para manejar esta lógica.
private Response createUserInKeycloak(UserRequest userRequest){
  UsersResource userResource = keycloakClient.getUsersResource();
  UserRepresentation userRepresentation = createUserRepresentationFromRequest(userRequest);
  return userResource.create(userRepresentation);
}
  1. Extración de la lógica de mapeo de usuario:
    • Antes: La lógica para mapear un usuario de la petición a un objeto UserRepresentation estaba totalmente incrustada en el método.
    • Después: Se ha definido el método privado createUserRepresentationFromRequest(UserRequest userRequest) para manejar esta lógica.
private UserRepresentation createUserRepresentationFromRequest(UserRequest userRequest){
  UserRepresentation userRepresentation = new UserRepresentation();
  userRepresentation.setFirstName(userRequest.firstName());
  userRepresentation.setLastName(userRequest.lastName());
  userRepresentation.setEmail(userRequest.email());
  userRepresentation.setUsername(userRequest.username());
  userRepresentation.setEnabled(true);
  userRepresentation.setEmailVerified(true);
  userRepresentation.setCredentials(Collections.singletonList(createCredentialRepresentation(userRequest)));
  return userRepresentation;
}
  1. Extracción de la lógica de creación de credenciales:
    • Antes: La lógica para crear las credenciales de un usuario estaba incrustada en el método.
    • Después: Se creó el método privado createCredentialRepresentation(UserRequest userRequest) para manejar esta lógica.
private CredentialRepresentation createCredentialRepresentation(UserRequest userRequest) {
  CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
  credentialRepresentation.setTemporary(false);
  credentialRepresentation.setType(OAuth2Constants.PASSWORD);
  credentialRepresentation.setValue(userRequest.password());
  return credentialRepresentation;
}
  1. Extracción de la lógica post-creación:
    • Antes: Después de crear un usuario, había una lógica adicional para establecer la contraseña y asignar roles.
    • Después: Se creó el método privado handleUserCreated(Response response, UserRequest userRequest) para manejar esta lógica.
private void handleUserCreated(Response response, UserRequest userRequest) {
  String userId = extractUserIdFromResponse(response);
  log.info("User id: {}", userId);

  setUserPassword(userId, userRequest);
  assignUserRoles(userId, userRequest);
}
  1. **Extracción de la lógica para extraer el ID del usuario:
    • Antes: Se manejaba la lógica para extraer el ID del usuario también en el mismo método.
    • Después: Se creó el método privado extractUserIdFromResponse(Response response) para manejar esta lógica.
private String extractUserIdFromResponse(Response response) {
  String path = response.getLocation().getPath();
  return path.substring(path.lastIndexOf('/') + 1);
}
  1. Extracción de la lógica para establecer la contraseña del usuario:
    • Antes: Se manejaba la lógica para establecer la contraseña del usuario también en el mismo método.
    • Después: Se creó el método privado setUserPassword(String userId, UserRequest userRequest) para manejar esta lógica.
private void setUserPassword(String userId, UserRequest userRequest) {
  UsersResource usersResource = keycloakClient.getUsersResource();
  usersResource.get(userId).resetPassword(createCredentialRepresentation(userRequest));
}
  1. Extracción de la lógica para asignar roles al usuario:
    • Antes: Se manejaba la lógica para asignar roles al usuario también en el mismo método.
    • Después: Se creó el método privado assignUserRoles(String userId, UserRequest userRequest) para manejar esta lógica.
private void assignUserRoles(String userId, UserRequest userRequest) {
  RealmResource realmResource = keycloakClient.getRealmResource();
  List<RoleRepresentation> roles = determineRolesToAssign(userRequest, realmResource);
  realmResource.users().get(userId).roles().realmLevel().add(roles);
}
  1. Extracción de la lógica para determinar los roles a asignar al usuario:
    • Antes: Se manejaba la lógica para determinar los roles a asignar al usuario también en el mismo método.
    • Después: Se creó el método privado determineRolesToAssign(UserRequest userRequest, RealmResource realmResource) para manejar esta lógica.
private List<RoleRepresentation> determineRolesToAssign(UserRequest userRequest, RealmResource realmResource) {
  if (userRequest.roles() == null || userRequest.roles().isEmpty()) {
    return List.of(realmResource.roles().get("user").toRepresentation());
  } else {
    return realmResource.roles().list().stream()
        .filter(role -> userRequest.roles().stream().anyMatch(roleName -> roleName.equalsIgnoreCase(role.getName())))
        .collect(Collectors.toList());
  }
}

Finalmente se nos ha quedado un método mucho más legible y fácil de entender.

updateUser(String userId, UserRequest userRequest)

Vamos a echar un vistazo a como está implementado el método updateUser(String userId, UserRequest userRequest):

public void updateUser(String userId, UserRequest userRepresentation) {
  log.info("Updating user");
  log.info("Realm: {}", KeycloakClient.getRealmResource().toRepresentation().getRealm());
  CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
  credentialRepresentation.setTemporary(false);
  credentialRepresentation.setType(OAuth2Constants.PASSWORD);
  credentialRepresentation.setValue(userRepresentation.password());

  UserRepresentation user = new UserRepresentation();
  user.setFirstName(userRepresentation.firstName());
  user.setLastName(userRepresentation.lastName());
  user.setEmail(userRepresentation.email());
  user.setUsername(userRepresentation.username());
  user.setEnabled(true);
  user.setEmailVerified(true);
  user.setCredentials(Collections.singletonList(credentialRepresentation));

  UserResource userResource = KeycloakClient.getUsersResource().get(userId);
  userResource.update(user);
}

Vamos a aplicar los mismos pasos que en el método anterior, y analizar como podemos dejar este método un poco más legible:

  1. Extracción de la Lógica de Mapeo de Usuario:
    • Antes: La lógica para mapear un usuario de la petición a un objeto UserRepresentation estaba en el método.
    • Después: Se utilizó el método privado createUserRepresentationFromRequest(UserRequest userRequest) para manejar esta lógica, así reutilizamos la lógica que ya teníamos en el método createUser.
  2. Extracción de la lógica de creación de credenciales:
    • Antes: La lógica para crear una representación de las credenciales estaba incrustada en el método updateUser.
    • Después: Se utilizó el método privado createCredentialRepresentation(UserRequest userRequest) para manejar esta lógica, reutilizando código que ya habíamos extraído en el método createUser.

Finalmente nuestro método queda de esta manera, sustancialmente con menos líneas de código y aprovechando el código anterior:

public void updateUser(String userId, UserRequest userRepresentation) {
  log.info("Updating user");
  logRealmInfo();
  UserResource userResource = keycloakClient.getUsersResource().get(userId);
  userResource.update(createUserRepresentationFromRequest(userRepresentation));
}

Y finalmente se nos queda una clase más legible y más fácil de entender:

@Service
@Slf4j
@AllArgsConstructor
public class KeycloakService {
private final static int STATUS_CREATED = 201;
private final static int STATUS_CONFLICT = 409;
private KeycloakClient keycloakClient;

public List<UserRepresentation> findAllUsers() {
  log.info("Finding all users");
  logRealmInfo();
  return keycloakClient.getRealmResource().users().list();
}

public List<UserRepresentation> searchByUsername(String username) {
  return keycloakClient.getRealmResource().users().searchByUsername(username, true);
}

public String createUser(@NonNull UserRequest userRequest) {
  log.info("Creating user");
  logRealmInfo();
  Response response = createUserInKeycloak(userRequest);

  if (response.getStatus() == STATUS_CREATED) {
    handleUserCreated(response, userRequest);
    return "User created";
  } else if (response.getStatus() == STATUS_CONFLICT) {
    log.error("User already exists");
    return "User already exists";
  } else {
    log.error("Error creating user");
    return "Error creating user";
  }

}

public void deleteUser(String userId) {
  log.info("Deleting user");
  logRealmInfo();
  keycloakClient.getUsersResource().get(userId).remove();
}

public void updateUser(String userId, UserRequest userRepresentation) {
  log.info("Updating user");
  logRealmInfo();
  UserResource userResource = keycloakClient.getUsersResource().get(userId);
  userResource.update(createUserRepresentationFromRequest(userRepresentation));
}

private void logRealmInfo(){
  log.info("Realm: {}", keycloakClient.getRealmResource().toRepresentation().getRealm());
}

private Response createUserInKeycloak(UserRequest userRequest){
  UsersResource userResource = keycloakClient.getUsersResource();
  UserRepresentation userRepresentation = createUserRepresentationFromRequest(userRequest);
  return userResource.create(userRepresentation);
}

private UserRepresentation createUserRepresentationFromRequest(UserRequest userRequest){
  UserRepresentation userRepresentation = new UserRepresentation();
  userRepresentation.setFirstName(userRequest.firstName());
  userRepresentation.setLastName(userRequest.lastName());
  userRepresentation.setEmail(userRequest.email());
  userRepresentation.setUsername(userRequest.username());
  userRepresentation.setEnabled(true);
  userRepresentation.setEmailVerified(true);
  userRepresentation.setCredentials(Collections.singletonList(createCredentialRepresentation(userRequest)));
  return userRepresentation;
}

private CredentialRepresentation createCredentialRepresentation(UserRequest userRequest) {
  CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
  credentialRepresentation.setTemporary(false);
  credentialRepresentation.setType(OAuth2Constants.PASSWORD);
  credentialRepresentation.setValue(userRequest.password());
  return credentialRepresentation;
}

private void handleUserCreated(Response response, UserRequest userRequest) {
  String userId = extractUserIdFromResponse(response);
  log.info("User id: {}", userId);

  setUserPassword(userId, userRequest);
  assignUserRoles(userId, userRequest);
}

private String extractUserIdFromResponse(Response response) {
  String path = response.getLocation().getPath();
  return path.substring(path.lastIndexOf('/') + 1);
}

private void setUserPassword(String userId, UserRequest userRequest) {
  UsersResource usersResource = keycloakClient.getUsersResource();
  usersResource.get(userId).resetPassword(createCredentialRepresentation(userRequest));
}

private void assignUserRoles(String userId, UserRequest userRequest) {
  RealmResource realmResource = keycloakClient.getRealmResource();
  List<RoleRepresentation> roles = determineRolesToAssign(userRequest, realmResource);
  realmResource.users().get(userId).roles().realmLevel().add(roles);
}

private List<RoleRepresentation> determineRolesToAssign(UserRequest userRequest, RealmResource realmResource) {
  if (userRequest.roles() == null || userRequest.roles().isEmpty()) {
    return List.of(realmResource.roles().get("user").toRepresentation());
  } else {
    return realmResource.roles().list().stream()
        .filter(role -> userRequest.roles().stream().anyMatch(roleName -> roleName.equalsIgnoreCase(role.getName())))
        .collect(Collectors.toList());
  }
}

KeycloakController.java

En nuestro controlador, no hay mucho por hacer, pero podemos corregir unos pequeños errores de código:

@RestController
@RequestMapping("/api/users")
@AllArgsConstructor
public class KeycloakController {
private KeycloakService keycloakService;

@GetMapping("/all")
@PreAuthorize("hasRole('admin_client_role')")
public ResponseEntity<?> findAllUsers() {
  return ResponseEntity.ok(keycloakService.findAllUsers());
}

@GetMapping("/user/{username}")
@PreAuthorize("hasRole('admin_client_role')")
public ResponseEntity<?> searchByUsername(@PathVariable String username) {
  return ResponseEntity.ok(keycloakService.searchByUsername(username));
}

@PostMapping("/user/create")
@PreAuthorize("hasRole('admin_client_role')")
public ResponseEntity<?> createUser(@RequestBody UserRequest userRequest) throws URISyntaxException {
  String response = keycloakService.createUser(userRequest);
  return ResponseEntity.created(new URI("/api/users/user/create")).body(response);
}

@PutMapping("/user/update/{userId}")
@PreAuthorize("hasRole('admin_client_role')")
public ResponseEntity<?> updateUser(@PathVariable String userId, @RequestBody UserRequest userRequest) {
  keycloakService.updateUser(userId, userRequest);
  return ResponseEntity.ok("User updated successfully");
}

Hay una serie de cosas que podemos mejorar:

  1. Consistencia en las Rutas de la API:
    • Antes: Las rutas como /user/{username}, /user/create, /user/update/{userId}.
    • Después: Se elimina el segmento /user y usar rutas como /{username}, /create, /update/{userId}. Dado que ya está en el controlador (/api/users), es redundante.
  2. Respuestas de la API:
    • Antes: Se está devolviendo ResponseEntity<?>.
    • Después: Especifica el tipo de respuesta en lugar de usar un comodín. Por ejemplo, ResponseEntity<List> para findAllUsers().

Y finalmente queda de la siguiente manera:

@RestController
@RequestMapping("/api/users")
@AllArgsConstructor
public class KeycloakController {
  private KeycloakService keycloakService;

  @GetMapping("/all")
  @PreAuthorize("hasRole('admin_client_role')")
  public ResponseEntity<List<UserRepresentation>> findAll() {
    return ResponseEntity.ok(keycloakService.findAllUsers());
  }

  @GetMapping("/{username}")
  @PreAuthorize("hasRole('admin_client_role') or hasRole('user_client_role')")
  public ResponseEntity<UserRepresentation> searchByUsername(@PathVariable String username) {
    return ResponseEntity.ok(keycloakService.searchByUsername(username).get(0));
  }

  @PostMapping("/create")
  @PreAuthorize("hasRole('admin_client_role')")
  public ResponseEntity<ApiResponse> createUser(@RequestBody UserRequest userRequest) throws URISyntaxException {
    String userId = keycloakService.createUser(userRequest);
    URI location = new URI("/api/users/" + userId);
    return ResponseEntity.created(location).body(new ApiResponse("User created successfully", HttpStatus.CREATED.value()));
  }

  @PutMapping("/update/{userId}")
  @PreAuthorize("hasRole('admin_client_role')")
  public ResponseEntity<ApiResponse> updateUser(@PathVariable String userId, @RequestBody UserRequest userRequest) {
    keycloakService.updateUser(userId, userRequest);
    return ResponseEntity.ok(new ApiResponse("User updated successfully", HttpStatus.OK.value()));
  }
}

SecurityConfiguration.java

En esta clase haremos unos cambios menores:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

  private final JwtAuthenticationConverter jwtAuthenticationConverter;

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(authorize -> {
          authorize
              .requestMatchers("/api/auth/login").permitAll()
              .anyRequest().authenticated();
        })
        .oauth2ResourceServer(oauth2 -> {
          oauth2.jwt(jwtConfigurer -> {
            jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter);
          });
        })
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .build();
  }
}

Lo único que haremos será remover unas llaves que no son necesarias.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

  private final JwtAuthenticationConverter jwtAuthenticationConverter;

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(authorize ->
          authorize
              .requestMatchers("/api/auth/login").permitAll()
              .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 ->
          oauth2.jwt(jwtConfigurer ->
            jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter)))
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .build();
  }
}

Después de estos cambios, podremos pasar la aplicación por nuestro Scanner de SonarQube y ver que ya no tenemos errores, y que la calidad de nuestro código ha mejorado sustancialmente.

SonarQube analysis

Sin duda, hay muchas cosas que se pueden mejorar en esta implementación, tampoco soy un experto refactorizando código, ni tampoco es cuestión de hacer sobre ingeniería en este proyecto, pero si duda es una buena mejoría y un buen punto de partida para seguir mejorando.