11

I am trying to create a user via the Keycloak API, and I would like to assign a realm-level role to them when they are first added. However, it doesn't seem to work like the documentation says it should.

I know that I could simply make a second add-role-to-user API request after the initial create-user one, but:

  • The documentation indicates that I shouldn't need to do this.
  • The second API request could fail, leaving the user in an "incomplete" state.
  • It would make the code I'm writing more complex than it needs to be.

To test this in irb, using the keycloak Ruby gem, I first request an access token from Keycloak:

require 'keycloak'
json = Keycloak::Client.get_token_by_client_credentials
access_token = JSON.parse(json)['access_token']

All of the following create a user within Keycloak, but without the "owner" role:

Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: ['owner'] }, access_token)
Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: ['1fff5f5f-7357-4f73-b45d-65ccd01f3bc8'] }, access_token)
Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: ['{"id":"1fff5f5f-7357-4f73-b45d-65ccd01f3bc8","name":"owner","description":"Indicates that a user is the owner of an organisation.","composite":false,"clientRole":false,"containerId":"MyRealmName"}'] }, access_token)

Attempting to use a role-hash instead of a string causes an error:

Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: [{"id"=>"1fff5f5f-7357-4f73-b45d-65ccd01f3bc8", "name"=>"owner", "description"=>"Indicates that a user is the owner of an organisation.", "composite"=>false, "clientRole"=>false, "containerId"=>"MyRealmName"}] }, access_token)

Traceback (most recent call last):
      16: from /home/thomas/.rvm/rubies/ruby-2.6.3/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
      15: from (irb):8
      14: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/keycloak-3.0.0/lib/keycloak.rb:541:in `generic_post'
      13: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/keycloak-3.0.0/lib/keycloak.rb:943:in `generic_request'
      12: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/keycloak-3.0.0/lib/keycloak.rb:915:in `block in generic_request'
      11: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient.rb:71:in `post'
      10: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/request.rb:52:in `execute'
        9: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/request.rb:145:in `execute'
        8: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/request.rb:715:in `transmit'
        7: from /home/thomas/.rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/net/http.rb:920:in `start'
        6: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/request.rb:725:in `block in transmit'
        5: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/request.rb:807:in `process_result'
        4: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/keycloak-3.0.0/lib/keycloak.rb:916:in `block (2 levels) in generic_request'
        3: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/keycloak-3.0.0/lib/keycloak.rb:958:in `rescue_response'
        2: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/abstract_response.rb:103:in `return!'
        1: from /home/thomas/.rvm/gems/ruby-2.6.3/gems/rest-client-2.0.2/lib/restclient/abstract_response.rb:223:in `exception_with_response'
RestClient::InternalServerError (500 Internal Server Error)

Keycloak prints the following, indicating that - as expected - the roles should be an array of strings, not hashes:

08:53:27,889 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-22) Uncaught server error: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
at [Source: (io.undertow.servlet.spec.ServletInputStreamImpl); line: 1, column: 37] (through reference chain: org.keycloak.representations.idm.UserRepresentation["realmRoles"]->java.util.ArrayList[0])

The same thing happens if I pass a single string instead of an array, like:

Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: 'owner' }, access_token)

Am I doing something wrong, or is this simply a bug in the Keycloak API?

Reference

Similar questions

GoBusto
  • 4,632
  • 6
  • 28
  • 45

2 Answers2

15

You did nothing wrong. It is a bug in the Keycloak API.

This request should work:

Keycloak::Admin.generic_post('users', nil, { username: 'someone', realmRoles: ['owner'] }, access_token)

Unfortunately the API documentation is wrong because the 'realmRoles' attribute doesn't work when trying to create/update a user/group.

You can find more informations about the behavior on the official bug tracker of Keycloak :

For now the only solution is to make multiple requests on the API, using the RoleMappers to map a role to a user.

Documentation about those operations : https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_role_mapper_resource

Merlin
  • 193
  • 2
  • 9
  • The links seem to be broken unfortunately, along with the functionality which still doesn't work. – Martin01478 Apr 13 '21 at 15:26
  • @Martin01478 You need to replace the `/6.0/` part of the URL with a later version -- such as https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_role_mapper_resource -- because they remove older documentation as new versions come out. – GoBusto May 16 '22 at 19:24
  • Even though this question was asked in 2019, it seems the bug is still present with keycloak 18.0, if I am not mistaken. @GoBusto your answer is the only workaround for now I guess. (I am using keycloak-admin-client java library) – section117 May 17 '22 at 16:10
  • 1
    lol keycloack 21.0.0 this still dosent work – David Bister Jun 23 '23 at 11:10
1

The bug is still present in keycloak 19.0.1 even tho was reported in 2016.

So there is a work around as GoGusto suggested. First create the user and then add the roles to the user.

private void addRealmRoleToUser(String username, String role){
    UserRepresentation userRepresentation = keycloak.realm(REALM_NAME).users().search(username).get(0);
    UserResource userResource =
        keycloak.realm(REALM_NAME).users().get(userRepresentation.getId());
    List<RoleRepresentation> rolesToAdd =
        Arrays.asList(keycloak.realm(REALM_NAME).roles().get(TEST_ROLE).toRepresentation());
    userResource.roles().realmLevel().add(rolesToAdd);
}

Respectively userRepresentation.getRealmRoles() is not working as well. getRealmRoles has to be done with UserResouce class like its written in keycloak UserTest.java:

 RoleMappingResource userRoles = realm.users().get(userId).roles();
        userRoles.realmLevel().add(Collections.singletonList(realm.roles().get("realm-composite").toRepresentation()));
        userRoles.clientLevel(clientUuid).add(Collections
                .singletonList(realm.clients().get(clientUuid).roles().get("client-composite").toRepresentation()));

        // check state before making the direct assignments
        assertNames(userRoles.realmLevel().listAll(), "realm-composite", Constants.DEFAULT_ROLES_ROLE_PREFIX + "-test");
        assertNames(userRoles.realmLevel().listAvailable(), "realm-child", "realm-role-in-group",
                "admin", "customer-user-premium", "realm-composite-role",
                "sample-realm-role",
                "attribute-role", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
        assertNames(userRoles.realmLevel().listEffective(), "realm-composite", "realm-child", "realm-role-in-group",
                "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION,
                Constants.DEFAULT_ROLES_ROLE_PREFIX + "-test");
Alex P.
  • 61
  • 3