Issue
Versions : Keycloak 22.04, Spring-Boot 3 + Spring Security
I have a spring-boot service connected to an existing mongodb with a users collection. I made a KeycloakInitializerRunner that initializes keycloak with the current users in the mongodb collection. So far everything works great. I have mapped users to their client roles and groups (which I call affiliation). In the Keycloak UI I see everything setup perfectly.
@Slf4j
@RequiredArgsConstructor
@Component
public class KeycloakInitializerRunner implements CommandLineRunner {
private final KeycloakService keycloakService;
@Override
public void run(String... args) {
log.info("Initializing '{}' realm in Keycloak ...", keycloakService.getRealmName());
keycloakService.initializeKeycloak();
log.info("'{}' initialization completed successfully!", keycloakService.getRealmName());
}
}
In my KeycloakService, I have logic that reads a user from the userRepository and maps it to create a UserRepresentation. The same logic that the KeycloakInitializerRunner uses is shared with a UserController where I am trying to /user/createUser. When I send a request to create a new user with exactly the same logic, this time, everything works well except it does not add the appropriate client roles I have defined in the request.
I will put relevant code from my KeycloakService here:
public void initializeKeycloak() {
// Initialization logic
Optional<RealmRepresentation> representationOptional = keycloakAdmin.realms()
.findAll()
.stream()
.filter(r -> r.getRealm().equals(REALM_NAME))
.findAny();
if (representationOptional.isPresent()) {
log.info("Removing already pre-configured '{}' realm", REALM_NAME);
keycloakAdmin.realm(REALM_NAME).remove();
}
RealmRepresentation realmRepresentation = createRealmRepresentation();
Set<String> affiliations = userRepository.findAllAsStream()
.map(User::getAffiliation)
.filter(affiliation -> affiliation != null && affiliation.getName() != null)
.map(CcgAffiliation::getName)
.collect(Collectors.toSet());
List<GroupRepresentation> groupRepresentations = createGroupRepresentations(affiliations);
List<UserRepresentation> userRepresentations = createUserRepresentations(groupRepresentations);
createRealm(realmRepresentation, groupRepresentations, userRepresentations);
testKeycloakIntegration();
}
private List<UserRepresentation> createUserRepresentations(List<GroupRepresentation> groupRepresentations) {
return userRepository.findAllAsStream()
.map(user -> {
UserRepresentation userRepresentation = createUserRepresentation(user);
addUserToGroups(user, userRepresentation, groupRepresentations);
return userRepresentation;
})
.toList();
}
private UserRepresentation createUserRepresentation(User user) {
CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
credentialRepresentation.setType(CredentialRepresentation.PASSWORD);
credentialRepresentation.setValue("admin");
UserRepresentation userRepresentation = new UserRepresentation();
userRepresentation.setUsername(user.getUsername());
userRepresentation.setEmail(user.getEmail());
userRepresentation.setLastName(user.getLastName());
userRepresentation.setFirstName(user.getFirstName());
userRepresentation.setEnabled(true);
userRepresentation.setCredentials(List.of(credentialRepresentation));
userRepresentation.setClientRoles(getClientRoles(CLIENT_ID, user)); // FIXME: WHY DOES NOT WORK ON /USER/CREATEUSER ???
userRepresentation.setEmailVerified(true);
userRepresentation.setCreatedTimestamp(Instant.now().toEpochMilli());
return userRepresentation;
}
UserController
@PostMapping(value = "/user/registerUser", consumes = "multipart/form-data", produces = "application/json")
public ResponseEntity<Void> registerUser(
@RequestParam("user") String userTemp,
@RequestParam(value = "file", required = false) MultipartFile file
) {
try {
User user = StaticObjectMapper.toObject(userTemp, User.class);
// deleted irrelevant code
userRepository.save(user);
// Create user in Keycloak
keycloakService.createKeycloakUser(user);
return ResponseEntity.ok().build();
} catch (Exception e) {
LOGGER.error(e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
I know best practice is to have a single entity managing my users, but this is a proof of concept that is almost working for my use case. The issue here is Keycloak not acting the same when createKeycloakUser is called. Any help is welcome.
Solution
The problem was that in createRealm(...) the users are saved differently (Keycloak's admin API). However, my main issue was that the client has a clientId property as well as an id property. The Keycloak UI shows that the clientId is whatever you set it to be, for example whatever-app and the id was a random UUID generated by keycloak.
The fix to the problem :
ClientRepresentation clientRepresentation = new ClientRepresentation();
// Note : Keycloak clients have both a clientId and an id property. In this service clientId = id.
// The clientId is a given property from application.yml while id is a UUID generated by Keycloak
// In some API calls to Keycloak, it used the ID instead of the clientID i.e: realmResource.clients().get(ID)
// Hence it is crucial to set both to the same value (CLIENT_ID) to avoid complexity
clientRepresentation.setId(CLIENT_ID);
clientRepresentation.setClientId(CLIENT_ID);
Make sure to clientRepresentation.setId(CLIENT_ID); or in your case to use the id instead of clientId for operations like
RoleRepresentation clientRole = realmResource.clients().get(CLIENT_ID).roles().get(role).toRepresentation();
Answered By - wazowski
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.