This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 3ae2a370507e CAMEL-23453: camel-keycloak - add federated identity 
linking operations (#25091)
3ae2a370507e is described below

commit 3ae2a370507eb5a8b8b83a6e85686bc76d4252fb
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Jul 28 09:19:09 2026 +0200

    CAMEL-23453: camel-keycloak - add federated identity linking operations 
(#25091)
    
    * CAMEL-23453: camel-keycloak - add federated identity linking operations
    
    The component exposed user CRUD but not federated-identity (IdP) link
    management, which is needed for SSO scenarios where a Keycloak user is 
linked
    to an external identity-provider account, and for provisioning users with
    pre-linked accounts during a migration.
    
    Add three operations backed by the admin client's UserResource:
    
    - addFederatedIdentity - link a user to an identity provider account
    - removeFederatedIdentity - unlink a user from an identity provider
    - getFederatedIdentities - list all identity provider links for a user
    
    New headers: CamelKeycloakIdentityProvider, CamelKeycloakFederatedUserId and
    CamelKeycloakFederatedUsername. The username is optional: when it is not
    supplied the external user id is used, so the link is never created with a
    blank username (Keycloak surfaces that field in the admin console).
    
    addFederatedIdentity checks the returned status and fails with the status 
code
    when the link could not be created, rather than reporting success.
    
    Co-Authored-By: Claude Fable 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-23453: address review - AssertJ, assertThatThrownBy and 
package-private test
    
    Per project conventions for new test code, 
KeycloakProducerFederatedIdentityTest
    now uses package-private visibility (class and @Test methods), AssertJ
    assertions, and assertThatThrownBy(...).cause().hasMessageContaining(...) 
for the
    error-status and validation cases instead of a try/catch that would silently
    pass if the exception were never thrown. Adds the test-scoped assertj-core
    dependency (not previously on camel-keycloak's test classpath; version 
managed
    in the parent).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Fable 5 <[email protected]>
---
 .../apache/camel/catalog/components/keycloak.json  |  11 +-
 .../camel/catalog/docs/keycloak-component.adoc     |  64 ++++++
 components/camel-keycloak/pom.xml                  |   5 +
 .../apache/camel/component/keycloak/keycloak.json  |  11 +-
 .../src/main/docs/keycloak-component.adoc          |  64 ++++++
 .../component/keycloak/KeycloakConstants.java      |   9 +
 .../component/keycloak/KeycloakOperations.java     |   5 +-
 .../camel/component/keycloak/KeycloakProducer.java |  95 +++++++++
 .../KeycloakProducerFederatedIdentityTest.java     | 222 +++++++++++++++++++++
 .../dsl/KeycloakEndpointBuilderFactory.java        |  36 ++++
 10 files changed, 513 insertions(+), 9 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/keycloak.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/keycloak.json
index 455c2b089fce..d6b591ca0be0 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/keycloak.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/keycloak.json
@@ -43,7 +43,7 @@
     "ipAddress": { "index": 16, "kind": "property", "displayName": "Ip 
Address", "group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter events by IP 
address" },
     "keycloakClient": { "index": 17, "kind": "property", "displayName": 
"Keycloak Client", "group": "common", "label": "", "required": false, "type": 
"object", "javaType": "org.keycloak.admin.client.Keycloak", "deprecated": 
false, "deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "To use an existing 
configured Keycloak admin client" },
     "maxResults": { "index": 18, "kind": "property", "displayName": "Max 
Results", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 100, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Maximum number of events 
to retrieve per poll" },
-    "operation": { "index": 19, "kind": "property", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "cr [...]
+    "operation": { "index": 19, "kind": "property", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "cr [...]
     "operationTypes": { "index": 20, "kind": "property", "displayName": 
"Operation Types", "group": "common", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter admin events by 
operation types (comma-separated list, e.g., CREATE,UPDATE,DELETE)" },
     "password": { "index": 21, "kind": "property", "displayName": "Password", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Keycloak password" },
     "pojoRequest": { "index": 22, "kind": "property", "displayName": "Pojo 
Request", "group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "If we want to use a POJO 
request as body or not" },
@@ -59,7 +59,7 @@
     "autowiredEnabled": { "index": 32, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
   },
   "headers": {
-    "CamelKeycloakOperation": { "index": 0, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": 
"org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "createGroup", "de 
[...]
+    "CamelKeycloakOperation": { "index": 0, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": 
"org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "createGroup", "de 
[...]
     "CamelKeycloakRealmName": { "index": 1, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The realm name", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#REALM_NAME" },
     "CamelKeycloakUserId": { "index": 2, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The user ID", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#USER_ID" },
     "CamelKeycloakUsername": { "index": 3, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The username", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#USERNAME" },
@@ -122,7 +122,10 @@
     "CamelKeycloakOrganizationDescription": { "index": 60, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization description", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_DESCRIPTION"
 },
     "CamelKeycloakOrganizationRedirectUrl": { "index": 61, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization redirect URL", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_REDIRECT_URL"
 },
     "CamelKeycloakOrganizationDomain": { "index": 62, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization domain name", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_DOMAIN" },
-    "CamelKeycloakOrganizationSearch": { "index": 63, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Search query for organizations", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_SEARCH" }
+    "CamelKeycloakOrganizationSearch": { "index": 63, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Search query for organizations", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_SEARCH" },
+    "CamelKeycloakIdentityProvider": { "index": 64, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The identity provider alias for a 
federated identity link", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#IDENTITY_PROVIDER" },
+    "CamelKeycloakFederatedUserId": { "index": 65, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The user id at the external identity 
provider", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#FEDERATED_USER_ID" },
+    "CamelKeycloakFederatedUsername": { "index": 66, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The username at the external identity 
provider", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#FEDERATED_USERNAME" }
   },
   "properties": {
     "label": { "index": 0, "kind": "path", "displayName": "Label", "group": 
"common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Logical name" },
@@ -144,7 +147,7 @@
     "ipAddress": { "index": 16, "kind": "parameter", "displayName": "Ip 
Address", "group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter events by IP 
address" },
     "keycloakClient": { "index": 17, "kind": "parameter", "displayName": 
"Keycloak Client", "group": "common", "label": "", "required": false, "type": 
"object", "javaType": "org.keycloak.admin.client.Keycloak", "deprecated": 
false, "deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "To use an existing 
configured Keycloak admin client" },
     "maxResults": { "index": 18, "kind": "parameter", "displayName": "Max 
Results", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 100, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Maximum number of events 
to retrieve per poll" },
-    "operation": { "index": 19, "kind": "parameter", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "c [...]
+    "operation": { "index": 19, "kind": "parameter", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "c [...]
     "operationTypes": { "index": 20, "kind": "parameter", "displayName": 
"Operation Types", "group": "common", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter admin events by 
operation types (comma-separated list, e.g., CREATE,UPDATE,DELETE)" },
     "password": { "index": 21, "kind": "parameter", "displayName": "Password", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Keycloak password" },
     "pojoRequest": { "index": 22, "kind": "parameter", "displayName": "Pojo 
Request", "group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "If we want to use a POJO 
request as body or not" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-component.adoc
index fd3ff808b6d2..4980f305233e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-component.adoc
@@ -142,6 +142,70 @@ YAML::
 ----
 ====
 
+=== Federated Identity Operations
+
+Federated identity operations manage the links between a Keycloak user and an 
external identity provider account
+(Google, GitHub, a SAML provider, and so on). They are useful for SSO 
scenarios and for provisioning users with
+pre-linked IdP accounts during a migration.
+
+* `addFederatedIdentity` — link a user to an identity provider account.
+* `removeFederatedIdentity` — unlink a user from an identity provider.
+* `getFederatedIdentities` — list all identity provider links for a user.
+
+All three require `CamelKeycloakRealmName` and `CamelKeycloakUserId`. In 
addition:
+
+* `addFederatedIdentity` requires `CamelKeycloakIdentityProvider` (the IdP 
alias) and `CamelKeycloakFederatedUserId`
+  (the user id at the provider). `CamelKeycloakFederatedUsername` is optional 
— when it is not supplied the external
+  user id is used as the username.
+* `removeFederatedIdentity` requires `CamelKeycloakIdentityProvider`.
+
+`getFederatedIdentities` sets the out-message body to the 
`List<FederatedIdentityRepresentation>` of links.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:linkAccount")
+    .setHeader(KeycloakConstants.REALM_NAME, constant("myrealm"))
+    .setHeader(KeycloakConstants.USER_ID, constant("user-uuid"))
+    .setHeader(KeycloakConstants.IDENTITY_PROVIDER, constant("google"))
+    .setHeader(KeycloakConstants.FEDERATED_USER_ID, 
constant("google-account-id"))
+    .setHeader(KeycloakConstants.FEDERATED_USERNAME, 
constant("[email protected]"))
+    .to("keycloak:admin?operation=addFederatedIdentity");
+
+from("direct:listLinks")
+    .setHeader(KeycloakConstants.REALM_NAME, constant("myrealm"))
+    .setHeader(KeycloakConstants.USER_ID, constant("user-uuid"))
+    .to("keycloak:admin?operation=getFederatedIdentities");
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:linkAccount
+      steps:
+        - setHeader:
+            name: CamelKeycloakRealmName
+            constant: "myrealm"
+        - setHeader:
+            name: CamelKeycloakUserId
+            constant: "user-uuid"
+        - setHeader:
+            name: CamelKeycloakIdentityProvider
+            constant: "google"
+        - setHeader:
+            name: CamelKeycloakFederatedUserId
+            constant: "google-account-id"
+        - to:
+            uri: keycloak:admin?operation=addFederatedIdentity
+----
+====
+
 == Configuration Options
 
 [width="100%",cols="10%,10%,80%",options="header"]
diff --git a/components/camel-keycloak/pom.xml 
b/components/camel-keycloak/pom.xml
index 8c1664d9de45..0c0ae1bd2371 100644
--- a/components/camel-keycloak/pom.xml
+++ b/components/camel-keycloak/pom.xml
@@ -91,6 +91,11 @@
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/components/camel-keycloak/src/generated/resources/META-INF/org/apache/camel/component/keycloak/keycloak.json
 
b/components/camel-keycloak/src/generated/resources/META-INF/org/apache/camel/component/keycloak/keycloak.json
index 455c2b089fce..d6b591ca0be0 100644
--- 
a/components/camel-keycloak/src/generated/resources/META-INF/org/apache/camel/component/keycloak/keycloak.json
+++ 
b/components/camel-keycloak/src/generated/resources/META-INF/org/apache/camel/component/keycloak/keycloak.json
@@ -43,7 +43,7 @@
     "ipAddress": { "index": 16, "kind": "property", "displayName": "Ip 
Address", "group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter events by IP 
address" },
     "keycloakClient": { "index": 17, "kind": "property", "displayName": 
"Keycloak Client", "group": "common", "label": "", "required": false, "type": 
"object", "javaType": "org.keycloak.admin.client.Keycloak", "deprecated": 
false, "deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "To use an existing 
configured Keycloak admin client" },
     "maxResults": { "index": 18, "kind": "property", "displayName": "Max 
Results", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 100, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Maximum number of events 
to retrieve per poll" },
-    "operation": { "index": 19, "kind": "property", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "cr [...]
+    "operation": { "index": 19, "kind": "property", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "cr [...]
     "operationTypes": { "index": 20, "kind": "property", "displayName": 
"Operation Types", "group": "common", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter admin events by 
operation types (comma-separated list, e.g., CREATE,UPDATE,DELETE)" },
     "password": { "index": 21, "kind": "property", "displayName": "Password", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Keycloak password" },
     "pojoRequest": { "index": 22, "kind": "property", "displayName": "Pojo 
Request", "group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "If we want to use a POJO 
request as body or not" },
@@ -59,7 +59,7 @@
     "autowiredEnabled": { "index": 32, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
   },
   "headers": {
-    "CamelKeycloakOperation": { "index": 0, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": 
"org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "createGroup", "de 
[...]
+    "CamelKeycloakOperation": { "index": 0, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": 
"org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "createGroup", "de 
[...]
     "CamelKeycloakRealmName": { "index": 1, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The realm name", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#REALM_NAME" },
     "CamelKeycloakUserId": { "index": 2, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The user ID", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#USER_ID" },
     "CamelKeycloakUsername": { "index": 3, "kind": "header", "displayName": 
"", "group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The username", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#USERNAME" },
@@ -122,7 +122,10 @@
     "CamelKeycloakOrganizationDescription": { "index": 60, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization description", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_DESCRIPTION"
 },
     "CamelKeycloakOrganizationRedirectUrl": { "index": 61, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization redirect URL", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_REDIRECT_URL"
 },
     "CamelKeycloakOrganizationDomain": { "index": 62, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The organization domain name", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_DOMAIN" },
-    "CamelKeycloakOrganizationSearch": { "index": 63, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Search query for organizations", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_SEARCH" }
+    "CamelKeycloakOrganizationSearch": { "index": 63, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Search query for organizations", 
"constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#ORGANIZATION_SEARCH" },
+    "CamelKeycloakIdentityProvider": { "index": 64, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The identity provider alias for a 
federated identity link", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#IDENTITY_PROVIDER" },
+    "CamelKeycloakFederatedUserId": { "index": 65, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The user id at the external identity 
provider", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#FEDERATED_USER_ID" },
+    "CamelKeycloakFederatedUsername": { "index": 66, "kind": "header", 
"displayName": "", "group": "common", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The username at the external identity 
provider", "constantName": 
"org.apache.camel.component.keycloak.KeycloakConstants#FEDERATED_USERNAME" }
   },
   "properties": {
     "label": { "index": 0, "kind": "path", "displayName": "Label", "group": 
"common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Logical name" },
@@ -144,7 +147,7 @@
     "ipAddress": { "index": 16, "kind": "parameter", "displayName": "Ip 
Address", "group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter events by IP 
address" },
     "keycloakClient": { "index": 17, "kind": "parameter", "displayName": 
"Keycloak Client", "group": "common", "label": "", "required": false, "type": 
"object", "javaType": "org.keycloak.admin.client.Keycloak", "deprecated": 
false, "deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "To use an existing 
configured Keycloak admin client" },
     "maxResults": { "index": 18, "kind": "parameter", "displayName": "Max 
Results", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 100, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Maximum number of events 
to retrieve per poll" },
-    "operation": { "index": 19, "kind": "parameter", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "c [...]
+    "operation": { "index": 19, "kind": "parameter", "displayName": 
"Operation", "group": "common", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.keycloak.KeycloakOperations", "enum": [ 
"createRealm", "deleteRealm", "getRealm", "updateRealm", "createUser", 
"deleteUser", "getUser", "updateUser", "listUsers", "searchUsers", 
"createRole", "deleteRole", "getRole", "updateRole", "listRoles", 
"assignRoleToUser", "removeRoleFromUser", "getUserRoles", "c [...]
     "operationTypes": { "index": 20, "kind": "parameter", "displayName": 
"Operation Types", "group": "common", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Filter admin events by 
operation types (comma-separated list, e.g., CREATE,UPDATE,DELETE)" },
     "password": { "index": 21, "kind": "parameter", "displayName": "Password", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "Keycloak password" },
     "pojoRequest": { "index": 22, "kind": "parameter", "displayName": "Pojo 
Request", "group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.keycloak.KeycloakConfiguration", 
"configurationField": "configuration", "description": "If we want to use a POJO 
request as body or not" },
diff --git a/components/camel-keycloak/src/main/docs/keycloak-component.adoc 
b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
index fd3ff808b6d2..4980f305233e 100644
--- a/components/camel-keycloak/src/main/docs/keycloak-component.adoc
+++ b/components/camel-keycloak/src/main/docs/keycloak-component.adoc
@@ -142,6 +142,70 @@ YAML::
 ----
 ====
 
+=== Federated Identity Operations
+
+Federated identity operations manage the links between a Keycloak user and an 
external identity provider account
+(Google, GitHub, a SAML provider, and so on). They are useful for SSO 
scenarios and for provisioning users with
+pre-linked IdP accounts during a migration.
+
+* `addFederatedIdentity` — link a user to an identity provider account.
+* `removeFederatedIdentity` — unlink a user from an identity provider.
+* `getFederatedIdentities` — list all identity provider links for a user.
+
+All three require `CamelKeycloakRealmName` and `CamelKeycloakUserId`. In 
addition:
+
+* `addFederatedIdentity` requires `CamelKeycloakIdentityProvider` (the IdP 
alias) and `CamelKeycloakFederatedUserId`
+  (the user id at the provider). `CamelKeycloakFederatedUsername` is optional 
— when it is not supplied the external
+  user id is used as the username.
+* `removeFederatedIdentity` requires `CamelKeycloakIdentityProvider`.
+
+`getFederatedIdentities` sets the out-message body to the 
`List<FederatedIdentityRepresentation>` of links.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:linkAccount")
+    .setHeader(KeycloakConstants.REALM_NAME, constant("myrealm"))
+    .setHeader(KeycloakConstants.USER_ID, constant("user-uuid"))
+    .setHeader(KeycloakConstants.IDENTITY_PROVIDER, constant("google"))
+    .setHeader(KeycloakConstants.FEDERATED_USER_ID, 
constant("google-account-id"))
+    .setHeader(KeycloakConstants.FEDERATED_USERNAME, 
constant("[email protected]"))
+    .to("keycloak:admin?operation=addFederatedIdentity");
+
+from("direct:listLinks")
+    .setHeader(KeycloakConstants.REALM_NAME, constant("myrealm"))
+    .setHeader(KeycloakConstants.USER_ID, constant("user-uuid"))
+    .to("keycloak:admin?operation=getFederatedIdentities");
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:linkAccount
+      steps:
+        - setHeader:
+            name: CamelKeycloakRealmName
+            constant: "myrealm"
+        - setHeader:
+            name: CamelKeycloakUserId
+            constant: "user-uuid"
+        - setHeader:
+            name: CamelKeycloakIdentityProvider
+            constant: "google"
+        - setHeader:
+            name: CamelKeycloakFederatedUserId
+            constant: "google-account-id"
+        - to:
+            uri: keycloak:admin?operation=addFederatedIdentity
+----
+====
+
 == Configuration Options
 
 [width="100%",cols="10%,10%,80%",options="header"]
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakConstants.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakConstants.java
index 34814ac14915..1100ed1cc1ee 100644
--- 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakConstants.java
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakConstants.java
@@ -221,6 +221,15 @@ public final class KeycloakConstants {
     @Metadata(description = "Search query for organizations", javaType = 
"String")
     public static final String ORGANIZATION_SEARCH = 
"CamelKeycloakOrganizationSearch";
 
+    @Metadata(description = "The identity provider alias for a federated 
identity link", javaType = "String")
+    public static final String IDENTITY_PROVIDER = 
"CamelKeycloakIdentityProvider";
+
+    @Metadata(description = "The user id at the external identity provider", 
javaType = "String")
+    public static final String FEDERATED_USER_ID = 
"CamelKeycloakFederatedUserId";
+
+    @Metadata(description = "The username at the external identity provider", 
javaType = "String")
+    public static final String FEDERATED_USERNAME = 
"CamelKeycloakFederatedUsername";
+
     private KeycloakConstants() {
         // Utility class
     }
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakOperations.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakOperations.java
index 1c5317c5d345..104f731c84dd 100644
--- 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakOperations.java
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakOperations.java
@@ -133,5 +133,8 @@ public enum KeycloakOperations {
     listOrganizationMembers,
     linkOrganizationIdentityProvider,
     unlinkOrganizationIdentityProvider,
-    listOrganizationIdentityProviders
+    listOrganizationIdentityProviders,
+    addFederatedIdentity,
+    removeFederatedIdentity,
+    getFederatedIdentities
 }
diff --git 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakProducer.java
 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakProducer.java
index 88dfc5251094..0a9913dc2cbb 100644
--- 
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakProducer.java
+++ 
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakProducer.java
@@ -47,6 +47,7 @@ import 
org.keycloak.authorization.client.resource.AuthorizationResource;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ClientScopeRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 import org.keycloak.representations.idm.MemberRepresentation;
@@ -78,6 +79,8 @@ public class KeycloakProducer extends DefaultProducer {
     public static final String MISSING_CLIENT_ID = "Client ID must be 
specified";
     public static final String MISSING_CLIENT_UUID = "Client UUID must be 
specified";
     public static final String MISSING_PASSWORD = "Password must be specified";
+    public static final String MISSING_IDENTITY_PROVIDER = "Identity provider 
alias must be specified";
+    public static final String MISSING_FEDERATED_USER_ID = "Federated user ID 
must be specified";
     public static final String MISSING_ORGANIZATION_ID = "Organization ID must 
be specified";
     public static final String MISSING_ORGANIZATION_NAME = "Organization name 
must be specified";
     public static final String MISSING_ORGANIZATION_MEMBER_ID = "Organization 
member (user) ID must be specified";
@@ -400,6 +403,15 @@ public class KeycloakProducer extends DefaultProducer {
             case listOrganizationIdentityProviders:
                 
listOrganizationIdentityProviders(getEndpoint().getKeycloakClient(), exchange);
                 break;
+            case addFederatedIdentity:
+                addFederatedIdentity(getEndpoint().getKeycloakClient(), 
exchange);
+                break;
+            case removeFederatedIdentity:
+                removeFederatedIdentity(getEndpoint().getKeycloakClient(), 
exchange);
+                break;
+            case getFederatedIdentities:
+                getFederatedIdentities(getEndpoint().getKeycloakClient(), 
exchange);
+                break;
             default:
                 throw new IllegalArgumentException("Unsupported operation: " + 
operation);
         }
@@ -2907,6 +2919,89 @@ public class KeycloakProducer extends DefaultProducer {
         message.setBody(idps);
     }
 
+    private void addFederatedIdentity(Keycloak keycloakClient, Exchange 
exchange) {
+        String realmName = 
exchange.getIn().getHeader(KeycloakConstants.REALM_NAME, String.class);
+        if (ObjectHelper.isEmpty(realmName)) {
+            throw new IllegalArgumentException(MISSING_REALM_NAME);
+        }
+
+        String userId = exchange.getIn().getHeader(KeycloakConstants.USER_ID, 
String.class);
+        if (ObjectHelper.isEmpty(userId)) {
+            throw new IllegalArgumentException(MISSING_USER_ID);
+        }
+
+        String identityProvider = 
exchange.getIn().getHeader(KeycloakConstants.IDENTITY_PROVIDER, String.class);
+        if (ObjectHelper.isEmpty(identityProvider)) {
+            throw new IllegalArgumentException(MISSING_IDENTITY_PROVIDER);
+        }
+
+        String federatedUserId = 
exchange.getIn().getHeader(KeycloakConstants.FEDERATED_USER_ID, String.class);
+        if (ObjectHelper.isEmpty(federatedUserId)) {
+            throw new IllegalArgumentException(MISSING_FEDERATED_USER_ID);
+        }
+
+        FederatedIdentityRepresentation federatedIdentity = new 
FederatedIdentityRepresentation();
+        federatedIdentity.setIdentityProvider(identityProvider);
+        federatedIdentity.setUserId(federatedUserId);
+
+        // Keycloak shows the external username in the admin console; fall 
back to the external user id when the
+        // caller does not provide one, so the link is never created with a 
blank username.
+        String federatedUsername = 
exchange.getIn().getHeader(KeycloakConstants.FEDERATED_USERNAME, String.class);
+        
federatedIdentity.setUserName(ObjectHelper.isNotEmpty(federatedUsername) ? 
federatedUsername : federatedUserId);
+
+        try (Response response
+                = 
keycloakClient.realm(realmName).users().get(userId).addFederatedIdentity(identityProvider,
+                        federatedIdentity)) {
+            if (response.getStatus() < 200 || response.getStatus() >= 300) {
+                throw new IllegalStateException(
+                        "Failed to add federated identity, status: " + 
response.getStatus());
+            }
+        }
+
+        Message message = getMessageForResponse(exchange);
+        message.setBody("Federated identity added successfully");
+    }
+
+    private void removeFederatedIdentity(Keycloak keycloakClient, Exchange 
exchange) {
+        String realmName = 
exchange.getIn().getHeader(KeycloakConstants.REALM_NAME, String.class);
+        if (ObjectHelper.isEmpty(realmName)) {
+            throw new IllegalArgumentException(MISSING_REALM_NAME);
+        }
+
+        String userId = exchange.getIn().getHeader(KeycloakConstants.USER_ID, 
String.class);
+        if (ObjectHelper.isEmpty(userId)) {
+            throw new IllegalArgumentException(MISSING_USER_ID);
+        }
+
+        String identityProvider = 
exchange.getIn().getHeader(KeycloakConstants.IDENTITY_PROVIDER, String.class);
+        if (ObjectHelper.isEmpty(identityProvider)) {
+            throw new IllegalArgumentException(MISSING_IDENTITY_PROVIDER);
+        }
+
+        
keycloakClient.realm(realmName).users().get(userId).removeFederatedIdentity(identityProvider);
+
+        Message message = getMessageForResponse(exchange);
+        message.setBody("Federated identity removed successfully");
+    }
+
+    private void getFederatedIdentities(Keycloak keycloakClient, Exchange 
exchange) {
+        String realmName = 
exchange.getIn().getHeader(KeycloakConstants.REALM_NAME, String.class);
+        if (ObjectHelper.isEmpty(realmName)) {
+            throw new IllegalArgumentException(MISSING_REALM_NAME);
+        }
+
+        String userId = exchange.getIn().getHeader(KeycloakConstants.USER_ID, 
String.class);
+        if (ObjectHelper.isEmpty(userId)) {
+            throw new IllegalArgumentException(MISSING_USER_ID);
+        }
+
+        List<FederatedIdentityRepresentation> federatedIdentities
+                = 
keycloakClient.realm(realmName).users().get(userId).getFederatedIdentity();
+
+        Message message = getMessageForResponse(exchange);
+        message.setBody(federatedIdentities);
+    }
+
     public static Message getMessageForResponse(final Exchange exchange) {
         return exchange.getMessage();
     }
diff --git 
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/KeycloakProducerFederatedIdentityTest.java
 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/KeycloakProducerFederatedIdentityTest.java
new file mode 100644
index 000000000000..2676133b020f
--- /dev/null
+++ 
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/KeycloakProducerFederatedIdentityTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.keycloak;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.ws.rs.core.Response;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the federated identity (identity provider link) operations.
+ */
+class KeycloakProducerFederatedIdentityTest extends CamelTestSupport {
+
+    @BindToRegistry("keycloakClient")
+    private Keycloak keycloakClient = Mockito.mock(Keycloak.class);
+
+    private RealmResource realmResource = Mockito.mock(RealmResource.class);
+    private UsersResource usersResource = Mockito.mock(UsersResource.class);
+    private UserResource userResource = Mockito.mock(UserResource.class);
+    private Response response = Mockito.mock(Response.class);
+
+    private void stubUserLookup() {
+        when(keycloakClient.realm(anyString())).thenReturn(realmResource);
+        when(realmResource.users()).thenReturn(usersResource);
+        when(usersResource.get(anyString())).thenReturn(userResource);
+    }
+
+    private Map<String, Object> headers(String... keyValues) {
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(KeycloakConstants.REALM_NAME, "testRealm");
+        headers.put(KeycloakConstants.USER_ID, "user-1");
+        for (int i = 0; i < keyValues.length; i += 2) {
+            headers.put(keyValues[i], keyValues[i + 1]);
+        }
+        return headers;
+    }
+
+    @Test
+    void testAddFederatedIdentity() throws Exception {
+        stubUserLookup();
+        when(response.getStatus()).thenReturn(204);
+        when(userResource.addFederatedIdentity(anyString(), 
any(FederatedIdentityRepresentation.class)))
+                .thenReturn(response);
+
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeaders("direct:addFederatedIdentity", null,
+                headers(KeycloakConstants.IDENTITY_PROVIDER, "google",
+                        KeycloakConstants.FEDERATED_USER_ID, "google-123",
+                        KeycloakConstants.FEDERATED_USERNAME, 
"[email protected]"));
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        ArgumentCaptor<FederatedIdentityRepresentation> captor
+                = 
ArgumentCaptor.forClass(FederatedIdentityRepresentation.class);
+        verify(userResource).addFederatedIdentity(eq("google"), 
captor.capture());
+
+        FederatedIdentityRepresentation sent = captor.getValue();
+        assertThat(sent.getIdentityProvider()).isEqualTo("google");
+        assertThat(sent.getUserId()).isEqualTo("google-123");
+        assertThat(sent.getUserName()).isEqualTo("[email protected]");
+    }
+
+    @Test
+    void testAddFederatedIdentityFallsBackToUserIdAsUsername() throws 
Exception {
+        stubUserLookup();
+        when(response.getStatus()).thenReturn(204);
+        when(userResource.addFederatedIdentity(anyString(), 
any(FederatedIdentityRepresentation.class)))
+                .thenReturn(response);
+
+        template.sendBodyAndHeaders("direct:addFederatedIdentity", null,
+                headers(KeycloakConstants.IDENTITY_PROVIDER, "github",
+                        KeycloakConstants.FEDERATED_USER_ID, "gh-42"));
+
+        ArgumentCaptor<FederatedIdentityRepresentation> captor
+                = 
ArgumentCaptor.forClass(FederatedIdentityRepresentation.class);
+        verify(userResource).addFederatedIdentity(eq("github"), 
captor.capture());
+
+        // no username supplied, so the external user id is used rather than 
leaving the link with a blank username
+        assertThat(captor.getValue().getUserName()).isEqualTo("gh-42");
+    }
+
+    @Test
+    void testAddFederatedIdentityFailsOnErrorStatus() throws Exception {
+        stubUserLookup();
+        when(response.getStatus()).thenReturn(409);
+        when(userResource.addFederatedIdentity(anyString(), 
any(FederatedIdentityRepresentation.class)))
+                .thenReturn(response);
+
+        assertThatThrownBy(() -> 
template.sendBodyAndHeaders("direct:addFederatedIdentity", null,
+                headers(KeycloakConstants.IDENTITY_PROVIDER, "google",
+                        KeycloakConstants.FEDERATED_USER_ID, "google-123")))
+                .cause()
+                .hasMessageContaining("409");
+    }
+
+    @Test
+    void testRemoveFederatedIdentity() throws Exception {
+        stubUserLookup();
+
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeaders("direct:removeFederatedIdentity", null,
+                headers(KeycloakConstants.IDENTITY_PROVIDER, "google"));
+
+        MockEndpoint.assertIsSatisfied(context);
+        verify(userResource).removeFederatedIdentity("google");
+    }
+
+    @Test
+    void testGetFederatedIdentities() throws Exception {
+        stubUserLookup();
+
+        FederatedIdentityRepresentation link = new 
FederatedIdentityRepresentation();
+        link.setIdentityProvider("google");
+        link.setUserId("google-123");
+        link.setUserName("[email protected]");
+        when(userResource.getFederatedIdentity()).thenReturn(List.of(link));
+
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeaders("direct:getFederatedIdentities", null, 
headers());
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> body = 
mock.getExchanges().get(0).getMessage().getBody(List.class);
+        assertThat(body).hasSize(1);
+        assertThat(((FederatedIdentityRepresentation) 
body.get(0)).getIdentityProvider()).isEqualTo("google");
+    }
+
+    @Test
+    void testMissingIdentityProvider() throws Exception {
+        assertThatThrownBy(() -> 
template.sendBodyAndHeaders("direct:addFederatedIdentity", null,
+                headers(KeycloakConstants.FEDERATED_USER_ID, "google-123")))
+                .cause()
+                .hasMessageContaining("Identity provider alias must be 
specified");
+    }
+
+    @Test
+    void testMissingFederatedUserId() throws Exception {
+        assertThatThrownBy(() -> 
template.sendBodyAndHeaders("direct:addFederatedIdentity", null,
+                headers(KeycloakConstants.IDENTITY_PROVIDER, "google")))
+                .cause()
+                .hasMessageContaining("Federated user ID must be specified");
+    }
+
+    @Test
+    void testMissingUserId() throws Exception {
+        assertThatThrownBy(() -> 
template.sendBodyAndHeader("direct:getFederatedIdentities", null,
+                KeycloakConstants.REALM_NAME, "testRealm"))
+                .cause()
+                .hasMessageContaining("User ID must be specified");
+    }
+
+    @Test
+    void testMissingRealmName() throws Exception {
+        assertThatThrownBy(() -> 
template.sendBodyAndHeader("direct:getFederatedIdentities", null,
+                KeycloakConstants.USER_ID, "user-1"))
+                .cause()
+                .hasMessageContaining("Realm name must be specified");
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:addFederatedIdentity")
+                        
.to("keycloak:test?keycloakClient=#keycloakClient&operation=addFederatedIdentity")
+                        .to("mock:result");
+
+                from("direct:removeFederatedIdentity")
+                        
.to("keycloak:test?keycloakClient=#keycloakClient&operation=removeFederatedIdentity")
+                        .to("mock:result");
+
+                from("direct:getFederatedIdentities")
+                        
.to("keycloak:test?keycloakClient=#keycloakClient&operation=getFederatedIdentities")
+                        .to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/KeycloakEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/KeycloakEndpointBuilderFactory.java
index fff6f8bcd0fe..91451123d1c9 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/KeycloakEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/KeycloakEndpointBuilderFactory.java
@@ -3299,6 +3299,42 @@ public interface KeycloakEndpointBuilderFactory {
         public String keycloakOrganizationSearch() {
             return "CamelKeycloakOrganizationSearch";
         }
+        /**
+         * The identity provider alias for a federated identity link.
+         * 
+         * The option is a: {@code String} type.
+         * 
+         * Group: common
+         * 
+         * @return the name of the header {@code KeycloakIdentityProvider}.
+         */
+        public String keycloakIdentityProvider() {
+            return "CamelKeycloakIdentityProvider";
+        }
+        /**
+         * The user id at the external identity provider.
+         * 
+         * The option is a: {@code String} type.
+         * 
+         * Group: common
+         * 
+         * @return the name of the header {@code KeycloakFederatedUserId}.
+         */
+        public String keycloakFederatedUserId() {
+            return "CamelKeycloakFederatedUserId";
+        }
+        /**
+         * The username at the external identity provider.
+         * 
+         * The option is a: {@code String} type.
+         * 
+         * Group: common
+         * 
+         * @return the name of the header {@code KeycloakFederatedUsername}.
+         */
+        public String keycloakFederatedUsername() {
+            return "CamelKeycloakFederatedUsername";
+        }
     }
     static KeycloakEndpointBuilder endpointBuilder(String componentName, 
String path) {
         class KeycloakEndpointBuilderImpl extends AbstractEndpointBuilder 
implements KeycloakEndpointBuilder, AdvancedKeycloakEndpointBuilder {

Reply via email to