diff --git a/changelogs/fragments/3610-fix-keycloak-client-diff-bugs-when-sorting.yml b/changelogs/fragments/3610-fix-keycloak-client-diff-bugs-when-sorting.yml
new file mode 100644
index 0000000000..ebbd6015d4
--- /dev/null
+++ b/changelogs/fragments/3610-fix-keycloak-client-diff-bugs-when-sorting.yml
@@ -0,0 +1,2 @@
+bugfixes:
+  - keycloak_client - update the check mode to not show differences resulting from sorting and default values relating to the properties, ``redirectUris``, ``attributes``, and ``protocol_mappers`` (https://github.com/ansible-collections/community.general/pull/3610).
diff --git a/plugins/modules/identity/keycloak/keycloak_client.py b/plugins/modules/identity/keycloak/keycloak_client.py
index 4309918f79..82cdab8b6c 100644
--- a/plugins/modules/identity/keycloak/keycloak_client.py
+++ b/plugins/modules/identity/keycloak/keycloak_client.py
@@ -685,6 +685,36 @@ from ansible_collections.community.general.plugins.module_utils.identity.keycloa
 from ansible.module_utils.basic import AnsibleModule
 
 
+def normalise_cr(clientrep, remove_ids=False):
+    """ Re-sorts any properties where the order so that diff's is minimised, and adds default values where appropriate so that the
+    the change detection is more effective.
+
+    :param clientrep: the clientrep dict to be sanitized
+    :param remove_ids: If set to true, then the unique ID's of objects is removed to make the diff and checks for changed
+                       not alert when the ID's of objects are not usually known, (e.g. for protocol_mappers)
+    :return: normalised clientrep dict
+    """
+    # Avoid the dict passed in to be modified
+    clientrep = clientrep.copy()
+
+    if 'attributes' in clientrep:
+        clientrep['attributes'] = list(sorted(clientrep['attributes']))
+
+    if 'redirectUris' in clientrep:
+        clientrep['redirectUris'] = list(sorted(clientrep['redirectUris']))
+
+    if 'protocolMappers' in clientrep:
+        clientrep['protocolMappers'] = sorted(clientrep['protocolMappers'], key=lambda x: (x.get('name'), x.get('protocol'), x.get('protocolMapper')))
+        for mapper in clientrep['protocolMappers']:
+            if remove_ids:
+                mapper.pop('id', None)
+
+            # Set to a default value.
+            mapper['consentRequired'] = mapper.get('consentRequired', False)
+
+    return clientrep
+
+
 def sanitize_cr(clientrep):
     """ Removes probably sensitive details from a client representation.
 
@@ -697,7 +727,7 @@ def sanitize_cr(clientrep):
     if 'attributes' in result:
         if 'saml.signing.private.key' in result['attributes']:
             result['attributes']['saml.signing.private.key'] = 'no_log'
-    return result
+    return normalise_cr(result)
 
 
 def main():
@@ -865,10 +895,12 @@ def main():
 
             if module.check_mode:
                 # We can only compare the current client with the proposed updates we have
+                before_norm = normalise_cr(before_client, remove_ids=True)
+                desired_norm = normalise_cr(desired_client, remove_ids=True)
                 if module._diff:
-                    result['diff'] = dict(before=sanitize_cr(before_client),
-                                          after=sanitize_cr(desired_client))
-                result['changed'] = (before_client != desired_client)
+                    result['diff'] = dict(before=sanitize_cr(before_norm),
+                                          after=sanitize_cr(desired_norm))
+                result['changed'] = (before_norm != desired_norm)
 
                 module.exit_json(**result)
 
diff --git a/tests/integration/targets/keycloak_client/README.md b/tests/integration/targets/keycloak_client/README.md
new file mode 100644
index 0000000000..06c2a4b414
--- /dev/null
+++ b/tests/integration/targets/keycloak_client/README.md
@@ -0,0 +1,11 @@
+The integration test can be performed as follows:
+
+```
+# 1. Start docker-compose:
+docker-compose -f tests/integration/targets/keycloak_client/docker-compose.yml stop
+docker-compose -f tests/integration/targets/keycloak_client/docker-compose.yml rm -f -v
+docker-compose -f tests/integration/targets/keycloak_client/docker-compose.yml up -d
+
+# 2. Run the integration tests:
+ansible-test integration keycloak_client --allow-unsupported -v
+```
diff --git a/tests/integration/targets/keycloak_client/docker-compose.yml b/tests/integration/targets/keycloak_client/docker-compose.yml
new file mode 100644
index 0000000000..d14a331e48
--- /dev/null
+++ b/tests/integration/targets/keycloak_client/docker-compose.yml
@@ -0,0 +1,26 @@
+version: '3.4'
+
+services:
+  postgres:
+    image: postgres:9.6
+    restart: always
+    environment:
+      POSTGRES_USER: postgres
+      POSTGRES_DB: postgres
+      POSTGRES_PASSWORD: postgres
+
+  keycloak:
+    image: jboss/keycloak:12.0.4
+    ports:
+      - 8080:8080
+
+    environment:
+      DB_VENDOR: postgres
+      DB_ADDR: postgres
+      DB_DATABASE: postgres
+      DB_USER: postgres
+      DB_SCHEMA: public
+      DB_PASSWORD: postgres
+
+      KEYCLOAK_USER: admin
+      KEYCLOAK_PASSWORD: password
diff --git a/tests/integration/targets/keycloak_client/tasks/main.yml b/tests/integration/targets/keycloak_client/tasks/main.yml
new file mode 100644
index 0000000000..322fc3e2f4
--- /dev/null
+++ b/tests/integration/targets/keycloak_client/tasks/main.yml
@@ -0,0 +1,59 @@
+---
+- name: Delete realm
+  community.general.keycloak_realm: "{{ auth_args | combine(call_args) }}"
+  vars:
+    call_args:
+      id: "{{ realm }}"
+      realm: "{{ realm }}"
+      state: absent
+
+- name: Create realm
+  community.general.keycloak_realm: "{{ auth_args | combine(call_args) }}"
+  vars:
+    call_args:
+      id: "{{ realm }}"
+      realm: "{{ realm }}"
+      state: present
+
+- name: Desire client
+  community.general.keycloak_client: "{{ auth_args | combine(call_args) }}"
+  vars:
+    call_args:
+      realm: "{{ realm }}"
+      client_id: "{{ client_id }}"
+      state: present
+      redirect_uris: '{{redirect_uris1}}'
+      attributes: '{{client_attributes1}}'
+      protocol_mappers: '{{protocol_mappers1}}'
+  register: desire_client_not_present
+
+- name: Desire client again with same props
+  community.general.keycloak_client: "{{ auth_args | combine(call_args) }}"
+  vars:
+    call_args:
+      realm: "{{ realm }}"
+      client_id: "{{ client_id }}"
+      state: present
+      redirect_uris: '{{redirect_uris1}}'
+      attributes: '{{client_attributes1}}'
+      protocol_mappers: '{{protocol_mappers1}}'
+  register: desire_client_when_present_and_same
+
+- name: Check client again with same props
+  community.general.keycloak_client: "{{ auth_args | combine(call_args) }}"
+  check_mode: yes
+  vars:
+    call_args:
+      realm: "{{ realm }}"
+      client_id: "{{ client_id }}"
+      state: present
+      redirect_uris: '{{redirect_uris1}}'
+      attributes: '{{client_attributes1}}'
+      protocol_mappers: '{{protocol_mappers1}}'
+  register: check_client_when_present_and_same
+
+- name: Assert changes not detected in last two tasks (desire when same, and check)
+  assert:
+    that:
+      - desire_client_when_present_and_same is not changed
+      - check_client_when_present_and_same is not changed
diff --git a/tests/integration/targets/keycloak_client/vars/main.yml b/tests/integration/targets/keycloak_client/vars/main.yml
new file mode 100644
index 0000000000..0b1555e4bb
--- /dev/null
+++ b/tests/integration/targets/keycloak_client/vars/main.yml
@@ -0,0 +1,57 @@
+---
+url: http://localhost:8080/auth
+admin_realm: master
+admin_user: admin
+admin_password: password
+realm: myrealm
+client_id: myclient
+role: myrole
+description_1: desc 1
+description_2: desc 2
+
+auth_args:
+  auth_keycloak_url: "{{ url }}"
+  auth_realm: "{{ admin_realm }}"
+  auth_username: "{{ admin_user }}"
+  auth_password: "{{ admin_password }}"
+
+redirect_uris1:
+  - "http://example.c.com/"
+  - "http://example.b.com/"
+  - "http://example.a.com/"
+
+client_attributes1: {"backchannel.logout.session.required": true, "backchannel.logout.revoke.offline.tokens": false}
+
+protocol_mappers1:
+  - name: 'email'
+    protocol: 'openid-connect'
+    protocolMapper: 'oidc-usermodel-property-mapper'
+    config:
+      "claim.name": "email"
+      "user.attribute": "email"
+      "jsonType.label": "String"
+      "id.token.claim": "true"
+      "access.token.claim": "true"
+      "userinfo.token.claim": "true"
+
+  - name: 'email_verified'
+    protocol: 'openid-connect'
+    protocolMapper: 'oidc-usermodel-property-mapper'
+    config:
+      "claim.name": "email_verified"
+      "user.attribute": "emailVerified"
+      "jsonType.label": "boolean"
+      "id.token.claim": "true"
+      "access.token.claim": "true"
+      "userinfo.token.claim": "true"
+
+  - name: 'family_name'
+    protocol: 'openid-connect'
+    protocolMapper: 'oidc-usermodel-property-mapper'
+    config:
+      "claim.name": "family_name"
+      "user.attribute": "lastName"
+      "jsonType.label": "String"
+      "id.token.claim": "true"
+      "access.token.claim": "true"
+      "userinfo.token.claim": "true"