0

I want to add a custom field to Keycloak user that is the MD5 hash of the user's email after the user is created.

I also searched for Keycloak user's custom fields, but it seemed like they weren't able to be programmed. I'm thinking of developing a Keycloak wrapper, but it would be great if there were a built-in solution already.

Is it possible to do so?

Tan Nguyen
  • 70
  • 8

2 Answers2

1

User's Attribute can save user email's MD5 hash value. Also Keycloak API support search by hash value.

User Update API

PUT {Keycloak URL}/admin/realms/{realm}/users/{user-id}

In Body

{
  "id": <user id>,
  "username": <user name>,
  "attributes": { "MD5": [ <user email MD5 hash >] }
}

Search User by attribute

GET {Keycloak URL}/admin/realms/{realm}/users?q={attribute key}:{attribute value}

Example, search by user's MD5 value

GET http://localhost:8080/auth/admin/realms/test/users?q=MD5:3b7c8c7791f4f4c7cdd712635277a1f2

Demo using node.js

const axios = require('axios')
const crypto = require('crypto')

const getMasterToken = async () => {
    try {
        const response = await axios.post(
            url = 'http://localhost:8080/auth/realms/master/protocol/openid-connect/token',
            data = new URLSearchParams({
                'client_id': 'admin-cli',
                'username': 'admin',
                'password': 'admin',
                'grant_type': 'password'
            }),
            config = {
                headers:
                {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            })
        return Promise.resolve(response.data.access_token)
    } catch (error) {
        return Promise.reject(error)
    }
}

const getUser = async (token, username) => {
    try {
        const response = await axios.get(
            url = `http://localhost:8080/auth/admin/realms/test/users?username=${username}`,
            config = {
                headers: {
                    'Accept-Encoding': 'application/json',
                    'Authorization': `Bearer ${token}`,
                }
            }
        );
        return Promise.resolve(response.data[0])
    } catch (error) {
        return Promise.reject(error)
    }
}


const addUserAttribute = async (token, user_data) => {
    try {
        const MD5 = crypto.createHash('md5').update(`${user_data.email}`).digest("hex")
        const newUserData = {
            "id": user_data.id,
            "username": user_data.username,
            "attributes": { "MD5": [MD5] }
        }
        const response = await axios.put(
            url = `http://localhost:8080/auth/admin/realms/test/users/${user_data.id}`,
            data = newUserData,
            config = {
                headers:
                {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${token}`,
                }
            })
        // response.status = 204 No Content. it means success to update
        return Promise.resolve(MD5)
    } catch (error) {
        return Promise.reject(error)
    }
}

const getUserByMD5 = async (token, MD5) => {
    try {
        const response = await axios.get(
            url = `http://localhost:8080/auth/admin/realms/test/users?q=MD5:${MD5}`,
            config = {
                headers: {
                    'Accept-Encoding': 'application/json',
                    'Authorization': `Bearer ${token}`,
                }
            }
        );
        return Promise.resolve(response.data)
    } catch (error) {
        return Promise.reject(error)
    }
}

getMasterToken()
    .then((token) => {
        getUser(token, 'user2')
            .then((user_data) => {
                console.log(JSON.stringify(user_data, null, 4))
                addUserAttribute(token, user_data)
                    .then((MD5) => {
                        console.log(`${user_data.username}'s MD5:` + MD5)
                        getUserByMD5(token, MD5)
                            .then((user_update_data) => {
                                console.log(JSON.stringify(user_update_data, null, 4))
                            })
                    })
            })
    })
    .catch(error => console.log(error));

Result

$ node update-user.js
{
    "id": "a3831b6a-63e5-471d-b71c-6c7d9f49ee47",
    "createdTimestamp": 1677063973333,
    "username": "user2",
    "enabled": true,
    "totp": false,
    "emailVerified": false,
    "firstName": "Tom",
    "lastName": "Cruise",
    "email": "user2@gmail.com",
    "disableableCredentialTypes": [],
    "requiredActions": [],
    "notBefore": 0,
    "access": {
        "manageGroupMembership": true,
        "view": true,
        "mapRoles": true,
        "impersonate": true,
        "manage": true
    }
}
user2's MD5:fa7c3fcb670a58aa3e90a391ea533c99
[
    {
        "id": "a3831b6a-63e5-471d-b71c-6c7d9f49ee47",
        "createdTimestamp": 1677063973333,
        "username": "user2",
        "enabled": true,
        "totp": false,
        "emailVerified": false,
        "firstName": "Tom",
        "lastName": "Cruise",
        "email": "user2@gmail.com",
        "attributes": {
            "MD5": [
                "fa7c3fcb670a58aa3e90a391ea533c99"
            ]
        },
        "disableableCredentialTypes": [],
        "requiredActions": [],
        "notBefore": 0,
        "access": {
            "manageGroupMembership": true,
            "view": true,
            "mapRoles": true,
            "impersonate": true,
            "manage": true
        }
    }
]

In Keycloak UI enter image description here

References

Searching for Keycloak user via attribute - searchForUserByUserAttribute - how is it fast?

Keycloak v.18: How to manipulate with users using Keycloak API

Bench Vue
  • 5,257
  • 2
  • 10
  • 14
  • Thanks, your idea is similar to mine about the wrapper. By saying built-in solution I mean only-keycloak involved solution. – Tan Nguyen Feb 23 '23 at 01:21
  • What you mean only built-in Keycloak? This demo shows only Keycloak API call. Not involve other library. It means not only by node.js, curl from terminal, Postman, python or other language call is possible. – Bench Vue Feb 23 '23 at 01:27
  • I mean the solution in which I can directly set a programmatically custom field in Keycloak instead of writing an API to calculate the field before sending it to Keycloak – Tan Nguyen Feb 23 '23 at 07:10
  • 1
    I think you talking about SPI[(Service Provider Interfaces)](https://www.keycloak.org/docs/latest/server_development/#_extensions_spi). It is customized functionally by Keyclock. It is possible , see that link. It is much complicate than my demo to calculation of hash code by single line. But both methods shulde be call by API. – Bench Vue Feb 23 '23 at 10:18
  • Thank you, it looks promising – Tan Nguyen Feb 23 '23 at 17:27
  • No I won't to demo SPI due to no experience and I think that approaches is not good methods for this case. – Bench Vue Feb 23 '23 at 17:40
0

In Java we have a dependency named keycloak-admin-client at first you should add this dependency in your pom.xml file . We use spring boot.

    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-admin-client</artifactId>
        <version>${keycloak.version}</version>
    </dependency>

    <dependency>
        <groupId>org.keycloak.bom</groupId>
        <artifactId>keycloak-adapter-bom</artifactId>
        <version>${keycloak.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

Keycloak-admin-client gives some apis to work with keycloak At first you should create a class for example named KeycloakService and a Dto named UserDTO like below

       @Data   
       public class UserDTO  {

          private static final long serialVersionUID = 1L;

          private String username;

          private String emailAddress;

          private String firstName;

          private String lastName;

          private String password;

          private String telephone;

          private String preferredLocal;

          private String accessibleViaMobile;

          private String fax;

          private String mobile;

          private String profile;

          private String accessProfile;

          private String notificationPreference;

          }

And keycloakService is like :

        @Service
        public class KeycloakService {

          @Autowired
          private final Keycloak keycloak;

          public UserDTO createUserInKeyCloak(UserDTO userDTO) {
          int statusId = 0;
          RealmResource realmResource = keycloak.realm(REALM);
          UsersResource userRessource = realmResource.users();
          UserRepresentation user = new UserRepresentation();
          user.setUsername(userDTO.getUsername());
          user.setEmail(userDTO.getEmailAddress());
          user.setFirstName(userDTO.getFirstName());
          user.setLastName(userDTO.getLastName());
          user.setEnabled(true);
          Map<String, List<String>> attr = new HashMap<>;
          List<String> attrString = new ArrayList<>;
          attrString.add("YOUR_MD5");
          attr.put("MD5",attrString)
          user.setAttributes(attr);
          // Create user
          Response result = userRessource.create(user);
          statusId = result.getStatus();
          if (statusId == 201) {
          String userId = 
          result.getLocation().getPath().replaceAll(".*/([^/]+)$", 
          "$1");
          System.out.println("User created with userId:" + 
          userId);
          // Define password credential
          CredentialRepresentation passwordCred = new 
          CredentialRepresentation();
          passwordCred.setTemporary(false);
          passwordCred.setType(CredentialRepresentation.PASSWORD);
          passwordCred.setValue(userDTO.getPassword());
          // Set password credential
          userRessource.get(userId).resetPassword(passwordCred);
          System.out.println("Username==" + userDTO.getUsername() 
          + " created in keycloak successfully");
          return userDTO;
          } else if (statusId == 409) {
            throw new CustomException(409, "the user is currently 
            exists");
          } else {
        throw new CustomException(statusId, "the user could not be 
         created in keycloak");
    }
 }
 }