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 0e7057e06cff CAMEL-24643: camel-opa - allow exchange properties in the 
OPA input document (#26187)
0e7057e06cff is described below

commit 0e7057e06cff595c85beafeb6999e1a09f477c29
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Sep 8 14:18:50 2026 +0200

    CAMEL-24643: camel-opa - allow exchange properties in the OPA input 
document (#26187)
    
    * CAMEL-24643: camel-opa - allow exchange properties in the OPA input 
document
    
    The input document was built from message headers only, but the
    authentication components deliberately keep the identity they verified in
    exchange properties: camel-keycloak stores the access token and its
    subject that way, and defaults preferPropertyOverHeader to true precisely
    because a header can be set by the caller.
    
    That left the two halves unable to compose. Authorizing an identity that
    an earlier step established meant copying it from an exchange property
    into a header first, which is the one channel the authentication step
    treats as untrusted.
    
    Adds an includeProperties option, mirroring includeHeaders, contributing a
    properties object to the input document. It defaults to empty rather than
    to everything: exchange properties mostly carry state between processors,
    so sending them all would be noise the policy has to wade through. When
    nothing is selected the properties key is omitted entirely, so the
    document an existing route sends is unchanged. Only custom properties are
    sent, never Camel's internal ones.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_017VuTsZ4GyaZyvP8zxFvCRu
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24643: address review - omit the empty "properties" key from the 
OPA input
    
    When includeProperties is configured but the exchange carries none of the 
listed
    properties, the input document added an empty "properties" map. A Rego 
policy
    doing has(input, "properties") would then see true and could make a silent 
wrong
    authorization decision. Only add "properties" when something was actually
    collected, and cover it with a test.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01HQ3k5eceny77eriMtgXhco
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../org/apache/camel/catalog/components/opa.json   | 24 ++++----
 .../apache/camel/catalog/docs/opa-component.adoc   | 49 ++++++++++++++-
 .../component/opa/OpaComponentConfigurer.java      |  6 ++
 .../camel/component/opa/OpaEndpointConfigurer.java |  6 ++
 .../camel/component/opa/OpaEndpointUriFactory.java |  3 +-
 .../org/apache/camel/component/opa/opa.json        | 24 ++++----
 .../camel-opa/src/main/docs/opa-component.adoc     | 49 ++++++++++++++-
 .../camel/component/opa/OpaConfiguration.java      | 21 +++++++
 .../apache/camel/component/opa/OpaEndpoint.java    |  2 +-
 .../camel/component/opa/OpaPolicyEvaluator.java    | 58 ++++++++++++++----
 .../component/opa/security/OpaSecurityPolicy.java  | 16 ++++-
 .../camel/component/opa/OpaInputDocumentTest.java  | 69 ++++++++++++++++++++++
 .../opa/security/OpaSecurityPolicyTest.java        | 16 +++++
 .../component/dsl/OpaComponentBuilderFactory.java  | 26 ++++++++
 .../endpoint/dsl/OpaEndpointBuilderFactory.java    | 24 ++++++++
 15 files changed, 351 insertions(+), 42 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
index 6ce49fbe7175..69aed6a2f817 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
@@ -28,12 +28,13 @@
     "configuration": { "index": 1, "kind": "property", "displayName": 
"Configuration", "group": "producer", "label": "", "required": false, "type": 
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration", 
"deprecated": false, "autowired": false, "secret": false, "description": "The 
component configuration." },
     "includeBody": { "index": 2, "kind": "property", "displayName": "Include 
Body", "group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to send the message body to OPA as 
part of the input document. Disabled by default: bodies can b [...]
     "includeHeaders": { "index": 3, "kind": "property", "displayName": 
"Include Headers", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "*", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of message header names 
to send to OPA in the input document. The defau [...]
-    "lazyStartProducer": { "index": 4, "kind": "property", "displayName": 
"Lazy Start Producer", "group": "producer", "label": "producer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Whether the producer should be started lazy (on the first message). By 
starting lazy you can use this to allow CamelContext and routes to startup in 
situations where a producer may otherwise fail [...]
-    "serverUrl": { "index": 5, "kind": "property", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defaul [...]
-    "autowiredEnabled": { "index": 6, "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 t [...]
-    "opaClient": { "index": 7, "kind": "property", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
-    "bearerToken": { "index": 8, "kind": "property", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that  [...]
-    "failOpen": { "index": 9, "kind": "property", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evaluat [...]
+    "includeProperties": { "index": 4, "kind": "property", "displayName": 
"Include Properties", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of exchange property 
names to send to OPA in the input document, or {code } for all of [...]
+    "lazyStartProducer": { "index": 5, "kind": "property", "displayName": 
"Lazy Start Producer", "group": "producer", "label": "producer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Whether the producer should be started lazy (on the first message). By 
starting lazy you can use this to allow CamelContext and routes to startup in 
situations where a producer may otherwise fail [...]
+    "serverUrl": { "index": 6, "kind": "property", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defaul [...]
+    "autowiredEnabled": { "index": 7, "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 t [...]
+    "opaClient": { "index": 8, "kind": "property", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
+    "bearerToken": { "index": 9, "kind": "property", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that  [...]
+    "failOpen": { "index": 10, "kind": "property", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
   },
   "headers": {
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The allow\/deny verdict of the policy 
evaluation. Always overwritten by the component, so a value set by an inbound 
message never survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
@@ -45,10 +46,11 @@
     "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "allow", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "The key to read the allow\/deny verdict from 
when the policy returns an object rather than a plain  [...]
     "includeBody": { "index": 2, "kind": "parameter", "displayName": "Include 
Body", "group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to send the message body to OPA as 
part of the input document. Disabled by default: bodies can  [...]
     "includeHeaders": { "index": 3, "kind": "parameter", "displayName": 
"Include Headers", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "*", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of message header names 
to send to OPA in the input document. The defa [...]
-    "serverUrl": { "index": 4, "kind": "parameter", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defau [...]
-    "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
-    "opaClient": { "index": 6, "kind": "parameter", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
-    "bearerToken": { "index": 7, "kind": "parameter", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that [...]
-    "failOpen": { "index": 8, "kind": "parameter", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
+    "includeProperties": { "index": 4, "kind": "parameter", "displayName": 
"Include Properties", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of exchange property 
names to send to OPA in the input document, or {code } for all o [...]
+    "serverUrl": { "index": 5, "kind": "parameter", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defau [...]
+    "lazyStartProducer": { "index": 6, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
+    "opaClient": { "index": 7, "kind": "parameter", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
+    "bearerToken": { "index": 8, "kind": "parameter", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that [...]
+    "failOpen": { "index": 9, "kind": "parameter", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index aa9dce34bf68..a42b872019e1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -95,6 +95,7 @@ Camel sends OPA an `input` document shaped like this:
 ------------------------------------------------------------
 {
   "headers": { "user": "alice", "CamelHttpMethod": "POST" },
+  "properties": { "CamelKeycloakTokenSubject": "alice" },
   "body": "...",
   "exchangeId": "1767DC33923810E-0000000000000000",
   "routeId": "orders"
@@ -124,6 +125,8 @@ alongside it shows the matching routes end to end.
 is enabled: bodies can be large or streaming, and most authorization decisions 
do not need them. When you do
 enable it on a streaming body, enable stream caching so the body is still 
readable by the rest of the route.
 
+`properties` is empty unless you ask for it — see <<authorizing-an-identity>> 
below.
+
 Header and body values that are not JSON-native are converted to their string 
form. That conversion is
 shallow: a `Map` or `List` value is passed through as-is, so anything 
non-JSON-native nested inside it is left
 for the OPA SDK's serializer to render. A policy that reads nested structures 
should not assume the same string
@@ -146,6 +149,48 @@ boolean verdict read out of it:
 Both headers are written on every evaluation, so a verdict set by an inbound 
message never survives into the
 route.
 
+[#authorizing-an-identity]
+== Authorizing an identity
+
+The component decides; it does not authenticate. Establish who the caller is 
first, then let the policy authorize
+the identity that step produced.
+
+The catch is that Camel's authentication components deliberately keep the 
identity they verified in *exchange
+properties* rather than in headers — `camel-keycloak` stores the access token 
and its subject that way and its
+`preferPropertyOverHeader` option defaults to `true`, precisely because a 
header can be set by the caller. So the
+identity you want to authorize is usually not in the header map.
+
+Name those properties in `includeProperties` and they arrive in the input 
document under `properties`:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+    .policy(keycloakPolicy)                                  // authenticates, 
stores the subject as a property
+    .to("opa:authz/orders/allow?includeProperties=CamelKeycloakTokenSubject")
+    .filter(header(OpaConstants.DECISION_ALLOW).isEqualTo(true))
+        .to("direct:handleOrder");
+------------------------------------------------------------
+
+and the policy reads them the same way it reads headers:
+
+[source,rego]
+------------------------------------------------------------
+allow if {
+    input.properties.CamelKeycloakTokenSubject == "alice"
+}
+------------------------------------------------------------
+
+`includeProperties` takes a comma-separated list, or `*` for all of them, and 
matches names case-insensitively.
+Unlike `includeHeaders` it is *empty by default*: exchange properties are 
mostly used to carry state between
+processors, so sending them all would be noise the policy has to wade through. 
Only custom properties are sent —
+Camel's own internal exchange properties are never included.
+
+Prefer this over copying the identity into a header before the `opa:` 
endpoint. A header is exactly the channel the
+authentication step treated as untrusted, so moving a verified identity into 
one to get it past this component
+undoes the check that produced it.
+
+`OpaSecurityPolicy` takes the same option through `setIncludeProperties`.
+
 == Failure handling
 
 The component fails closed. If the policy cannot be evaluated at all — the OPA 
server is unreachable, times out,
@@ -168,5 +213,5 @@ so the policy always returns a verdict.
 * OPA is a trusted component, typically running as a sidecar reachable only 
from the application. When it is not,
   put the connection on a trusted network and authenticate to it with 
`bearerToken`.
 * The component decides; it does not authenticate. Establish *who* the caller 
is first — with SPIFFE workload
-  identity, a verified JWT, or the surrounding transport's authentication — 
and let the policy authorize the
-  identity that step produced.
+  identity, a verified JWT, or the surrounding transport's authentication — 
and hand the policy that identity
+  through `includeProperties` rather than through a header, as 
<<authorizing-an-identity>> explains.
diff --git 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
index ad6b93cbe7f1..e41a4fe473e6 100644
--- 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
+++ 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaComponentConfigurer.java
@@ -43,6 +43,8 @@ public class OpaComponentConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": 
getOrCreateConfiguration(target).setIncludeBody(property(camelContext, 
boolean.class, value)); return true;
         case "includeheaders":
         case "includeHeaders": 
getOrCreateConfiguration(target).setIncludeHeaders(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "includeproperties":
+        case "includeProperties": 
getOrCreateConfiguration(target).setIncludeProperties(property(camelContext, 
java.lang.String.class, value)); return true;
         case "lazystartproducer":
         case "lazyStartProducer": 
target.setLazyStartProducer(property(camelContext, boolean.class, value)); 
return true;
         case "opaclient":
@@ -74,6 +76,8 @@ public class OpaComponentConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": return boolean.class;
         case "includeheaders":
         case "includeHeaders": return java.lang.String.class;
+        case "includeproperties":
+        case "includeProperties": return java.lang.String.class;
         case "lazystartproducer":
         case "lazyStartProducer": return boolean.class;
         case "opaclient":
@@ -101,6 +105,8 @@ public class OpaComponentConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": return 
getOrCreateConfiguration(target).isIncludeBody();
         case "includeheaders":
         case "includeHeaders": return 
getOrCreateConfiguration(target).getIncludeHeaders();
+        case "includeproperties":
+        case "includeProperties": return 
getOrCreateConfiguration(target).getIncludeProperties();
         case "lazystartproducer":
         case "lazyStartProducer": return target.isLazyStartProducer();
         case "opaclient":
diff --git 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
index 94c7d0294376..4adc1c32c46a 100644
--- 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
+++ 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointConfigurer.java
@@ -33,6 +33,8 @@ public class OpaEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": 
target.getConfiguration().setIncludeBody(property(camelContext, boolean.class, 
value)); return true;
         case "includeheaders":
         case "includeHeaders": 
target.getConfiguration().setIncludeHeaders(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "includeproperties":
+        case "includeProperties": 
target.getConfiguration().setIncludeProperties(property(camelContext, 
java.lang.String.class, value)); return true;
         case "lazystartproducer":
         case "lazyStartProducer": 
target.setLazyStartProducer(property(camelContext, boolean.class, value)); 
return true;
         case "opaclient":
@@ -61,6 +63,8 @@ public class OpaEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": return boolean.class;
         case "includeheaders":
         case "includeHeaders": return java.lang.String.class;
+        case "includeproperties":
+        case "includeProperties": return java.lang.String.class;
         case "lazystartproducer":
         case "lazyStartProducer": return boolean.class;
         case "opaclient":
@@ -85,6 +89,8 @@ public class OpaEndpointConfigurer extends 
PropertyConfigurerSupport implements
         case "includeBody": return target.getConfiguration().isIncludeBody();
         case "includeheaders":
         case "includeHeaders": return 
target.getConfiguration().getIncludeHeaders();
+        case "includeproperties":
+        case "includeProperties": return 
target.getConfiguration().getIncludeProperties();
         case "lazystartproducer":
         case "lazyStartProducer": return target.isLazyStartProducer();
         case "opaclient":
diff --git 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
index 5940cbbc4222..afd9836e8644 100644
--- 
a/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
+++ 
b/components/camel-opa/src/generated/java/org/apache/camel/component/opa/OpaEndpointUriFactory.java
@@ -24,12 +24,13 @@ public class OpaEndpointUriFactory extends 
org.apache.camel.support.component.En
     private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(9);
+        Set<String> props = new HashSet<>(10);
         props.add("allowKey");
         props.add("bearerToken");
         props.add("failOpen");
         props.add("includeBody");
         props.add("includeHeaders");
+        props.add("includeProperties");
         props.add("lazyStartProducer");
         props.add("opaClient");
         props.add("policyPath");
diff --git 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
index 6ce49fbe7175..69aed6a2f817 100644
--- 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
+++ 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
@@ -28,12 +28,13 @@
     "configuration": { "index": 1, "kind": "property", "displayName": 
"Configuration", "group": "producer", "label": "", "required": false, "type": 
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration", 
"deprecated": false, "autowired": false, "secret": false, "description": "The 
component configuration." },
     "includeBody": { "index": 2, "kind": "property", "displayName": "Include 
Body", "group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to send the message body to OPA as 
part of the input document. Disabled by default: bodies can b [...]
     "includeHeaders": { "index": 3, "kind": "property", "displayName": 
"Include Headers", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "*", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of message header names 
to send to OPA in the input document. The defau [...]
-    "lazyStartProducer": { "index": 4, "kind": "property", "displayName": 
"Lazy Start Producer", "group": "producer", "label": "producer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Whether the producer should be started lazy (on the first message). By 
starting lazy you can use this to allow CamelContext and routes to startup in 
situations where a producer may otherwise fail [...]
-    "serverUrl": { "index": 5, "kind": "property", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defaul [...]
-    "autowiredEnabled": { "index": 6, "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 t [...]
-    "opaClient": { "index": 7, "kind": "property", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
-    "bearerToken": { "index": 8, "kind": "property", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that  [...]
-    "failOpen": { "index": 9, "kind": "property", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evaluat [...]
+    "includeProperties": { "index": 4, "kind": "property", "displayName": 
"Include Properties", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of exchange property 
names to send to OPA in the input document, or {code } for all of [...]
+    "lazyStartProducer": { "index": 5, "kind": "property", "displayName": 
"Lazy Start Producer", "group": "producer", "label": "producer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Whether the producer should be started lazy (on the first message). By 
starting lazy you can use this to allow CamelContext and routes to startup in 
situations where a producer may otherwise fail [...]
+    "serverUrl": { "index": 6, "kind": "property", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defaul [...]
+    "autowiredEnabled": { "index": 7, "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 t [...]
+    "opaClient": { "index": 8, "kind": "property", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
+    "bearerToken": { "index": 9, "kind": "property", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that  [...]
+    "failOpen": { "index": 10, "kind": "property", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
   },
   "headers": {
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The allow\/deny verdict of the policy 
evaluation. Always overwritten by the component, so a value set by an inbound 
message never survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
@@ -45,10 +46,11 @@
     "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "allow", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "The key to read the allow\/deny verdict from 
when the policy returns an object rather than a plain  [...]
     "includeBody": { "index": 2, "kind": "parameter", "displayName": "Include 
Body", "group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to send the message body to OPA as 
part of the input document. Disabled by default: bodies can  [...]
     "includeHeaders": { "index": 3, "kind": "parameter", "displayName": 
"Include Headers", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "*", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of message header names 
to send to OPA in the input document. The defa [...]
-    "serverUrl": { "index": 4, "kind": "parameter", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defau [...]
-    "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
-    "opaClient": { "index": 6, "kind": "parameter", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
-    "bearerToken": { "index": 7, "kind": "parameter", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that [...]
-    "failOpen": { "index": 8, "kind": "parameter", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
+    "includeProperties": { "index": 4, "kind": "parameter", "displayName": 
"Include Properties", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of exchange property 
names to send to OPA in the input document, or {code } for all o [...]
+    "serverUrl": { "index": 5, "kind": "parameter", "displayName": "Server 
Url", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "http:\/\/localhost:8181", 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "The base URL of the OPA 
server, without the {code \/v1\/data} suffix. The defau [...]
+    "lazyStartProducer": { "index": 6, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
+    "opaClient": { "index": 7, "kind": "parameter", "displayName": "Opa 
Client", "group": "advanced", "label": "advanced", "required": false, "type": 
"object", "javaType": "com.styra.opa.OPAClient", "deprecated": false, 
"deprecationNote": "", "autowired": true, "secret": false, 
"configurationClass": "org.apache.camel.component.opa.OpaConfiguration", 
"configurationField": "configuration", "description": "An existing OPAClient to 
use. When set, serverUrl and bearerToken are ignored." },
+    "bearerToken": { "index": 8, "kind": "parameter", "displayName": "Bearer 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Bearer token sent to the OPA server in the 
Authorization header, for an OPA instance that [...]
+    "failOpen": { "index": 9, "kind": "parameter", "displayName": "Fail Open", 
"group": "security", "label": "security", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "security": "insecure:dev", "defaultValue": false, "configurationClass": 
"org.apache.camel.component.opa.OpaConfiguration", "configurationField": 
"configuration", "description": "Whether to allow the exchange to proceed when 
the policy cannot be evalua [...]
   }
 }
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index aa9dce34bf68..a42b872019e1 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -95,6 +95,7 @@ Camel sends OPA an `input` document shaped like this:
 ------------------------------------------------------------
 {
   "headers": { "user": "alice", "CamelHttpMethod": "POST" },
+  "properties": { "CamelKeycloakTokenSubject": "alice" },
   "body": "...",
   "exchangeId": "1767DC33923810E-0000000000000000",
   "routeId": "orders"
@@ -124,6 +125,8 @@ alongside it shows the matching routes end to end.
 is enabled: bodies can be large or streaming, and most authorization decisions 
do not need them. When you do
 enable it on a streaming body, enable stream caching so the body is still 
readable by the rest of the route.
 
+`properties` is empty unless you ask for it — see <<authorizing-an-identity>> 
below.
+
 Header and body values that are not JSON-native are converted to their string 
form. That conversion is
 shallow: a `Map` or `List` value is passed through as-is, so anything 
non-JSON-native nested inside it is left
 for the OPA SDK's serializer to render. A policy that reads nested structures 
should not assume the same string
@@ -146,6 +149,48 @@ boolean verdict read out of it:
 Both headers are written on every evaluation, so a verdict set by an inbound 
message never survives into the
 route.
 
+[#authorizing-an-identity]
+== Authorizing an identity
+
+The component decides; it does not authenticate. Establish who the caller is 
first, then let the policy authorize
+the identity that step produced.
+
+The catch is that Camel's authentication components deliberately keep the 
identity they verified in *exchange
+properties* rather than in headers — `camel-keycloak` stores the access token 
and its subject that way and its
+`preferPropertyOverHeader` option defaults to `true`, precisely because a 
header can be set by the caller. So the
+identity you want to authorize is usually not in the header map.
+
+Name those properties in `includeProperties` and they arrive in the input 
document under `properties`:
+
+[source,java]
+------------------------------------------------------------
+from("platform-http:/orders")
+    .policy(keycloakPolicy)                                  // authenticates, 
stores the subject as a property
+    .to("opa:authz/orders/allow?includeProperties=CamelKeycloakTokenSubject")
+    .filter(header(OpaConstants.DECISION_ALLOW).isEqualTo(true))
+        .to("direct:handleOrder");
+------------------------------------------------------------
+
+and the policy reads them the same way it reads headers:
+
+[source,rego]
+------------------------------------------------------------
+allow if {
+    input.properties.CamelKeycloakTokenSubject == "alice"
+}
+------------------------------------------------------------
+
+`includeProperties` takes a comma-separated list, or `*` for all of them, and 
matches names case-insensitively.
+Unlike `includeHeaders` it is *empty by default*: exchange properties are 
mostly used to carry state between
+processors, so sending them all would be noise the policy has to wade through. 
Only custom properties are sent —
+Camel's own internal exchange properties are never included.
+
+Prefer this over copying the identity into a header before the `opa:` 
endpoint. A header is exactly the channel the
+authentication step treated as untrusted, so moving a verified identity into 
one to get it past this component
+undoes the check that produced it.
+
+`OpaSecurityPolicy` takes the same option through `setIncludeProperties`.
+
 == Failure handling
 
 The component fails closed. If the policy cannot be evaluated at all — the OPA 
server is unreachable, times out,
@@ -168,5 +213,5 @@ so the policy always returns a verdict.
 * OPA is a trusted component, typically running as a sidecar reachable only 
from the application. When it is not,
   put the connection on a trusted network and authenticate to it with 
`bearerToken`.
 * The component decides; it does not authenticate. Establish *who* the caller 
is first — with SPIFFE workload
-  identity, a verified JWT, or the surrounding transport's authentication — 
and let the policy authorize the
-  identity that step produced.
+  identity, a verified JWT, or the surrounding transport's authentication — 
and hand the policy that identity
+  through `includeProperties` rather than through a header, as 
<<authorizing-an-identity>> explains.
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
index ec18c3777cf0..acf7c943d33c 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
@@ -34,6 +34,9 @@ public class OpaConfiguration implements Cloneable {
     @UriParam(defaultValue = "*")
     private String includeHeaders = "*";
 
+    @UriParam
+    private String includeProperties;
+
     @UriParam
     private boolean includeBody;
 
@@ -85,6 +88,24 @@ public class OpaConfiguration implements Cloneable {
         this.includeHeaders = includeHeaders;
     }
 
+    /**
+     * Comma-separated list of exchange property names to send to OPA in the 
input document, or {@code *} for all of
+     * them. Empty by default, so no properties are sent unless asked for.
+     * <p/>
+     * This is where the authentication components put the identity they 
verified: {@code camel-keycloak} stores the
+     * access token and its subject as exchange properties and prefers them 
over the equivalent headers, precisely
+     * because headers can be set by the caller. List those property names 
here to let a policy authorize the identity
+     * an earlier step established, instead of copying it into a header first. 
Only custom properties are sent; Camel's
+     * own internal exchange properties are never included.
+     */
+    public String getIncludeProperties() {
+        return includeProperties;
+    }
+
+    public void setIncludeProperties(String includeProperties) {
+        this.includeProperties = includeProperties;
+    }
+
     /**
      * Whether to send the message body to OPA as part of the input document. 
Disabled by default: bodies can be large
      * or streaming, and most authorization decisions only need headers. When 
enabled on a streaming body, enable stream
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
index 410238871527..9bb828df47fc 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java
@@ -62,7 +62,7 @@ public class OpaEndpoint extends DefaultEndpoint {
                 : 
OpaPolicyEvaluator.createClient(configuration.getServerUrl(), 
configuration.getBearerToken());
         evaluator = new OpaPolicyEvaluator(
                 opaClient, policyPath, configuration.getAllowKey(), 
configuration.getIncludeHeaders(),
-                configuration.isIncludeBody(), configuration.isFailOpen());
+                configuration.getIncludeProperties(), 
configuration.isIncludeBody(), configuration.isFailOpen());
     }
 
     @Override
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
index 6758e17ae1a3..332ce66dd8ba 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
@@ -38,21 +38,25 @@ public class OpaPolicyEvaluator {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(OpaPolicyEvaluator.class);
 
-    private static final String ALL_HEADERS = "*";
+    private static final String ALL_NAMES = "*";
 
     private final OPAClient client;
     private final String policyPath;
     private final String allowKey;
     private final Set<String> includedHeaders;
+    private final Set<String> includedProperties;
     private final boolean includeBody;
     private final boolean failOpen;
 
     public OpaPolicyEvaluator(OPAClient client, String policyPath, String 
allowKey, String includeHeaders,
-                              boolean includeBody, boolean failOpen) {
+                              String includeProperties, boolean includeBody, 
boolean failOpen) {
         this.client = ObjectHelper.notNull(client, "client");
         this.policyPath = ObjectHelper.notNull(policyPath, "policyPath");
         this.allowKey = ObjectHelper.isNotEmpty(allowKey) ? allowKey : "allow";
-        this.includedHeaders = parseIncludedHeaders(includeHeaders);
+        // headers default to all of them, exchange properties to none: 
properties are mostly used to carry
+        // state between processors, so sending them all would be noise the 
policy has to wade through
+        this.includedHeaders = parseNameFilter(includeHeaders, true);
+        this.includedProperties = parseNameFilter(includeProperties, false);
         this.includeBody = includeBody;
         this.failOpen = failOpen;
     }
@@ -111,7 +115,7 @@ public class OpaPolicyEvaluator {
             String name = entry.getKey();
             // never feed our own decision headers back in: a policy must not 
be able to read a verdict
             // that an inbound message claimed for itself
-            if (isDecisionHeader(name) || !isIncluded(name)) {
+            if (isDecisionHeader(name) || !isIncluded(includedHeaders, name)) {
                 continue;
             }
             Object value = toJsonSafe(exchange, entry.getValue());
@@ -120,6 +124,23 @@ public class OpaPolicyEvaluator {
             }
         }
         input.put("headers", headers);
+        if (includesAnything(includedProperties)) {
+            Map<String, Object> properties = new LinkedHashMap<>();
+            for (Map.Entry<String, Object> entry : 
exchange.getProperties().entrySet()) {
+                if (!isIncluded(includedProperties, entry.getKey())) {
+                    continue;
+                }
+                Object value = toJsonSafe(exchange, entry.getValue());
+                if (value != null) {
+                    properties.put(entry.getKey(), value);
+                }
+            }
+            // only expose "properties" when something was actually collected, 
so a Rego policy doing
+            // has(input, "properties") is not misled into seeing an (empty) 
identity that is not there
+            if (!properties.isEmpty()) {
+                input.put("properties", properties);
+            }
+        }
         if (includeBody) {
             input.put("body", toJsonSafe(exchange, 
exchange.getMessage().getBody()));
         }
@@ -156,8 +177,12 @@ public class OpaPolicyEvaluator {
         exchange.getMessage().setHeader(OpaConstants.POLICY_PATH, policyPath);
     }
 
-    private boolean isIncluded(String name) {
-        return includedHeaders == null || includedHeaders.contains(name);
+    private static boolean isIncluded(Set<String> filter, String name) {
+        return filter == null || filter.contains(name);
+    }
+
+    private static boolean includesAnything(Set<String> filter) {
+        return filter == null || !filter.isEmpty();
     }
 
     private static boolean isDecisionHeader(String name) {
@@ -167,20 +192,27 @@ public class OpaPolicyEvaluator {
     }
 
     /**
-     * @return the header names to include, or null when every header is 
included
+     * Parses a comma-separated name filter.
+     *
+     * @param  names         the configured value, or {@code *} for everything
+     * @param  emptyMeansAll whether leaving the option unset includes every 
name or none of them
+     * @return               the names to include, or null when every name is 
included
      */
-    private static Set<String> parseIncludedHeaders(String includeHeaders) {
-        if (ObjectHelper.isEmpty(includeHeaders) || 
ALL_HEADERS.equals(includeHeaders.trim())) {
+    private static Set<String> parseNameFilter(String names, boolean 
emptyMeansAll) {
+        if (ObjectHelper.isEmpty(names)) {
+            return emptyMeansAll ? null : Set.of();
+        }
+        if (ALL_NAMES.equals(names.trim())) {
             return null;
         }
-        Set<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
-        for (String name : includeHeaders.split(",")) {
+        Set<String> filter = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        for (String name : names.split(",")) {
             String trimmed = name.trim();
             if (!trimmed.isEmpty()) {
-                names.add(trimmed);
+                filter.add(trimmed);
             }
         }
-        return names;
+        return filter;
     }
 
     /**
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
index f1414e8d40f0..40c9ceb9c68e 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
@@ -49,6 +49,7 @@ public class OpaSecurityPolicy implements AuthorizationPolicy 
{
     private String policyPath;
     private String allowKey = "allow";
     private String includeHeaders = "*";
+    private String includeProperties;
     private boolean includeBody;
     private String bearerToken;
     private boolean failOpen;
@@ -71,7 +72,8 @@ public class OpaSecurityPolicy implements AuthorizationPolicy 
{
             if (opaClient == null) {
                 opaClient = OpaPolicyEvaluator.createClient(serverUrl, 
bearerToken);
             }
-            evaluator = new OpaPolicyEvaluator(opaClient, policyPath, 
allowKey, includeHeaders, includeBody, failOpen);
+            evaluator = new OpaPolicyEvaluator(
+                    opaClient, policyPath, allowKey, includeHeaders, 
includeProperties, includeBody, failOpen);
         }
     }
 
@@ -131,6 +133,18 @@ public class OpaSecurityPolicy implements 
AuthorizationPolicy {
         this.includeHeaders = includeHeaders;
     }
 
+    public String getIncludeProperties() {
+        return includeProperties;
+    }
+
+    /**
+     * Comma-separated list of exchange property names to send to OPA, or 
{@code *} for all of them. Empty by default.
+     * Use it to hand the policy an identity that an earlier authentication 
step stored as an exchange property.
+     */
+    public void setIncludeProperties(String includeProperties) {
+        this.includeProperties = includeProperties;
+    }
+
     public boolean isIncludeBody() {
         return includeBody;
     }
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
index cb0c7d7c8e37..c39ccb5d0693 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
@@ -129,6 +129,75 @@ class OpaInputDocumentTest extends CamelTestSupport {
         assertThat(headersOf(input).get("when")).isInstanceOf(String.class);
     }
 
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> propertiesOf(Map<String, Object> input) 
{
+        return (Map<String, Object>) input.get("properties");
+    }
+
+    @Test
+    void sendsNoExchangePropertiesByDefault() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT, e -> 
e.setProperty("subject", "alice"));
+
+        assertThat(input).doesNotContainKey("properties");
+    }
+
+    @Test
+    void sendsOnlyTheListedExchangeProperties() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=subject", e -> {
+            e.setProperty("subject", "alice");
+            e.setProperty("internalScratch", "not for the policy");
+        });
+
+        
assertThat(propertiesOf(input)).containsOnlyKeys("subject").containsEntry("subject",
 "alice");
+    }
+
+    @Test
+    void omitsPropertiesKeyWhenIncludeMatchesNothing() throws Exception {
+        // includeProperties is configured but the exchange carries none of 
the listed properties, so the
+        // "properties" key must be absent rather than present-and-empty 
(otherwise has(input, "properties") lies)
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=subject",
+                e -> e.setProperty("internalScratch", "not for the policy"));
+
+        assertThat(input).doesNotContainKey("properties");
+    }
+
+    @Test
+    void sendsEveryExchangePropertyWhenAskedForAll() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=*", e -> {
+            e.setProperty("subject", "alice");
+            e.setProperty("tenant", "acme");
+        });
+
+        assertThat(propertiesOf(input)).containsEntry("subject", 
"alice").containsEntry("tenant", "acme");
+    }
+
+    @Test
+    void matchesTheListedPropertyNamesCaseInsensitively() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=subject",
+                e -> e.setProperty("SUBJECT", "alice"));
+
+        assertThat(propertiesOf(input)).containsEntry("SUBJECT", "alice");
+    }
+
+    @Test
+    void convertsANonJsonPropertyValueToItsStringForm() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=when",
+                e -> e.setProperty("when", new Date(0)));
+
+        assertThat(propertiesOf(input).get("when")).isInstanceOf(String.class);
+    }
+
+    @Test
+    void keepsHeadersAndPropertiesInSeparateObjects() throws Exception {
+        Map<String, Object> input = inputSentFor(ENDPOINT + 
"&includeProperties=subject", e -> {
+            e.getMessage().setHeader("subject", "mallory");
+            e.setProperty("subject", "alice");
+        });
+
+        assertThat(headersOf(input)).containsEntry("subject", "mallory");
+        assertThat(propertiesOf(input)).containsEntry("subject", "alice");
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     void reportsTheRouteTheExchangeCameFrom() throws Exception {
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
index 5d07a686ddaf..55589ec971a6 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyTest.java
@@ -28,11 +28,13 @@ import org.apache.camel.component.opa.OpaConstants;
 import org.apache.camel.component.opa.OpaPolicyEvaluationException;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.anyMap;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 class OpaSecurityPolicyTest extends CamelTestSupport {
@@ -46,6 +48,7 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
     protected RouteBuilder createRouteBuilder() {
         policy.setPolicyPath(PATH);
         policy.setOpaClient(client);
+        policy.setIncludeProperties("subject");
         return new RouteBuilder() {
             @Override
             public void configure() {
@@ -111,6 +114,19 @@ class OpaSecurityPolicyTest extends CamelTestSupport {
         result.assertIsSatisfied();
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    void handsThePolicyAnIdentityCarriedAsAnExchangeProperty() throws 
Exception {
+        givenDecision(Boolean.TRUE);
+
+        template.request("direct:start", e -> e.setProperty("subject", 
"alice"));
+
+        ArgumentCaptor<Map<String, Object>> captor = 
ArgumentCaptor.forClass(Map.class);
+        verify(client).evaluate(eq(PATH), captor.capture(), eq(Object.class));
+        assertThat((Map<String, Object>) captor.getValue().get("properties"))
+                .containsEntry("subject", "alice");
+    }
+
     @Test
     void overwritesAVerdictClaimedByTheInboundMessage() throws Exception {
         givenDecision(Boolean.FALSE);
diff --git 
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
 
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
index 09cdea3d61bc..abc8d8031c24 100644
--- 
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
+++ 
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
@@ -128,6 +128,31 @@ public interface OpaComponentBuilderFactory {
             return this;
         }
     
+        /**
+         * Comma-separated list of exchange property names to send to OPA in 
the
+         * input document, or {code } for all of them. Empty by default, so no
+         * properties are sent unless asked for. This is where the
+         * authentication components put the identity they verified: {code
+         * camel-keycloak} stores the access token and its subject as exchange
+         * properties and prefers them over the equivalent headers, precisely
+         * because headers can be set by the caller. List those property names
+         * here to let a policy authorize the identity an earlier step
+         * established, instead of copying it into a header first. Only custom
+         * properties are sent; Camel's own internal exchange properties are
+         * never included.
+         * 
+         * The option is a: &lt;code&gt;java.lang.String&lt;/code&gt; type.
+         * 
+         * Group: producer
+         * 
+         * @param includeProperties the value to set
+         * @return the dsl builder
+         */
+        default OpaComponentBuilder includeProperties(java.lang.String 
includeProperties) {
+            doSetProperty("includeProperties", includeProperties);
+            return this;
+        }
+    
         
         /**
          * Whether the producer should be started lazy (on the first message).
@@ -270,6 +295,7 @@ public interface OpaComponentBuilderFactory {
             case "configuration": ((OpaComponent) 
component).setConfiguration((org.apache.camel.component.opa.OpaConfiguration) 
value); return true;
             case "includeBody": getOrCreateConfiguration((OpaComponent) 
component).setIncludeBody((boolean) value); return true;
             case "includeHeaders": getOrCreateConfiguration((OpaComponent) 
component).setIncludeHeaders((java.lang.String) value); return true;
+            case "includeProperties": getOrCreateConfiguration((OpaComponent) 
component).setIncludeProperties((java.lang.String) value); return true;
             case "lazyStartProducer": ((OpaComponent) 
component).setLazyStartProducer((boolean) value); return true;
             case "serverUrl": getOrCreateConfiguration((OpaComponent) 
component).setServerUrl((java.lang.String) value); return true;
             case "autowiredEnabled": ((OpaComponent) 
component).setAutowiredEnabled((boolean) value); return true;
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
index 9c526562e6e9..8b3f6f3bc950 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
@@ -118,6 +118,30 @@ public interface OpaEndpointBuilderFactory {
             doSetProperty("includeHeaders", includeHeaders);
             return this;
         }
+        /**
+         * Comma-separated list of exchange property names to send to OPA in 
the
+         * input document, or {code } for all of them. Empty by default, so no
+         * properties are sent unless asked for. This is where the
+         * authentication components put the identity they verified: {code
+         * camel-keycloak} stores the access token and its subject as exchange
+         * properties and prefers them over the equivalent headers, precisely
+         * because headers can be set by the caller. List those property names
+         * here to let a policy authorize the identity an earlier step
+         * established, instead of copying it into a header first. Only custom
+         * properties are sent; Camel's own internal exchange properties are
+         * never included.
+         * 
+         * The option is a: <code>java.lang.String</code> type.
+         * 
+         * Group: producer
+         * 
+         * @param includeProperties the value to set
+         * @return the dsl builder
+         */
+        default OpaEndpointBuilder includeProperties(String includeProperties) 
{
+            doSetProperty("includeProperties", includeProperties);
+            return this;
+        }
         /**
          * The base URL of the OPA server, without the {code /v1/data} suffix.
          * The default assumes OPA running as a sidecar on the standard port.

Reply via email to