This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 ac50a34b96c5 CAMEL-26831: Preserve request bodies during JWT-SVID
validation (#29638)
ac50a34b96c5 is described below
commit ac50a34b96c50bf8b0aa1752b387f6d7d978a913
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 21:45:39 2026 +0100
CAMEL-26831: Preserve request bodies during JWT-SVID validation (#29638)
Add opt-in body-preserving JWT-SVID validation and a reusable
SpiffeJwtSecurityPolicy. The policy exposes the immutable verified identity as
an exchange property; producer identity headers keep their existing contract.
The policy inherits the SPIFFE component configuration, with explicitly
configured policy client/socket settings taking precedence. Failures clear
stale identity, use sanitized authorization exceptions and log failure class
names without SDK messages or credentials. Shared audience parsing keeps the
configured-audience and Bearer-before-body security decisions.
Includes Java/XML/YAML documentation, generated metadata and DSL, and real
SPIRE policy tests covering request-body preservation, wrong-audience denial
and route restart.
Codex and Claude Code on behalf of oscerd.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-authored-by: Codex <[email protected]>
---
.../org/apache/camel/catalog/beans.properties | 1 +
.../catalog/beans/SpiffeJwtSecurityPolicy.json | 16 ++
.../apache/camel/catalog/components/spiffe.json | 24 +-
.../camel/catalog/docs/spiffe-component.adoc | 135 +++++++++
.../spiffe/SpiffeComponentConfigurer.java | 6 +
.../component/spiffe/SpiffeEndpointConfigurer.java | 6 +
.../component/spiffe/SpiffeEndpointUriFactory.java | 3 +-
.../spiffe/SpiffeJwtSecurityPolicyConfigurer.java | 73 +++++
.../org/apache/camel/component/spiffe/spiffe.json | 24 +-
.../services/org/apache/camel/bean.properties | 2 +-
.../apache/camel/bean/SpiffeJwtSecurityPolicy.json | 16 ++
....camel.component.spiffe.SpiffeJwtSecurityPolicy | 2 +
.../src/main/docs/spiffe-component.adoc | 135 +++++++++
.../component/spiffe/SpiffeConfiguration.java | 16 ++
.../camel/component/spiffe/SpiffeConstants.java | 8 +-
.../camel/component/spiffe/SpiffeJwtIdentity.java | 90 ++++++
.../component/spiffe/SpiffeJwtSecurityPolicy.java | 144 ++++++++++
.../spiffe/SpiffeJwtSecurityProcessor.java | 90 ++++++
.../camel/component/spiffe/SpiffeJwtValidator.java | 129 +++++++++
.../camel/component/spiffe/SpiffeProducer.java | 94 +-----
.../component/spiffe/SpiffeJwtValidationTest.java | 316 +++++++++++++++++++++
.../spiffe/integration/SpiffeWorkloadApiIT.java | 84 ++++++
.../java/org/apache/camel/util/ArtifactUtils.java | 2 +
.../dsl/SpiffeComponentBuilderFactory.java | 22 ++
.../endpoint/dsl/SpiffeEndpointBuilderFactory.java | 43 ++-
25 files changed, 1368 insertions(+), 113 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
index 10f0d5a67519..42d0d6092bca 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
@@ -56,6 +56,7 @@ RedisAggregationRepository
RedisKeyValueRepository
ShiroSecurityPolicy
SimpleScheduledRoutePolicy
+SpiffeJwtSecurityPolicy
SpiffeSSLContextParameters
SpiffeSecurityPolicy
SpringCacheIdempotentRepository
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeJwtSecurityPolicy.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeJwtSecurityPolicy.json
new file mode 100644
index 000000000000..aa7360e6dcc6
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/SpiffeJwtSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeJwtSecurityPolicy",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Spiffe Jwt Security Policy",
+ "description": "Validates a JWT-SVID and authorizes its SPIFFE identity
before a route segment, preserving the application body. A missing, rejected or
unauthorized token fails closed.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "audience": { "index": 0, "kind": "property",
"displayName": "Audience", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "Comma-separated audiences to validate against. A token is
accepted when it validates for any configured audience. Message headers cannot
override this check." }, "acceptedSpiffeIds": { "index": 1, "kind": "property",
"displayName": "Accepted Spiffe Ids", "r [...]
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
index 9e4d228ec454..69434d105cbc 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
@@ -28,18 +28,19 @@
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"lazyStartProducer": { "index": 2, "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 [...]
"operation": { "index": 3, "kind": "property", "displayName": "Operation",
"group": "producer", "label": "", "required": false, "type": "enum",
"javaType": "org.apache.camel.component.spiffe.SpiffeOperation", "enum": [
"fetchX509Svid", "fetchJwtSvid", "validateJwtSvid" ], "deprecated": false,
"autowired": false, "secret": false, "defaultValue": "fetchX509Svid",
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "descr [...]
- "autowiredEnabled": { "index": 4, "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 [...]
- "workloadApiClient": { "index": 5, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does not [...]
- "allowOperationHeader": { "index": 6, "kind": "property", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
- "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tcp [...]
- "x509Response": { "index": 8, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
+ "preserveBody": { "index": 4, "kind": "property", "displayName": "Preserve
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether validateJwtSvid leaves the application
body untouched and publishes the verified identit [...]
+ "autowiredEnabled": { "index": 5, "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 [...]
+ "workloadApiClient": { "index": 6, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does not [...]
+ "allowOperationHeader": { "index": 7, "kind": "property", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
+ "spiffeSocketPath": { "index": 8, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tcp [...]
+ "x509Response": { "index": 9, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
},
"headers": {
"CamelSpiffeOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.spiffe.SpiffeOperation or String", "deprecated":
false, "autowired": false, "secret": false, "description": "Overrides the
operation to be used by the producer. Ignored unless the endpoint sets
allowOperationHeader=true, because the operation decides whether the endpoint
validates a token or mints one.", "const [...]
"CamelSpiffeAudience": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The comma-separated audience(s) for the fetchJwtSvid operation.
Ignored by validateJwtSvid, which always validates against the configured
audience: there the audience is the check that binds the token to this
workload, not a parameter.", "constantName": "or [...]
"CamelSpiffeToken": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The JWT-SVID token to validate, for the validateJwtSvid
operation.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#TOKEN" },
"CamelSpiffeSpiffeId": { "index": 3, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The SPIFFE ID of the returned SVID.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#SPIFFE_ID" },
- "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "autowired": false, "secret": false,
"description": "The expiry of the returned SVID: the token expiry for
fetchJwtSvid, or the leaf certificate's notAfter for fetchX509Svid.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
+ "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "autowired": false, "secret": false,
"description": "The expiry of the returned SVID: the token expiry for
fetchJwtSvid or validateJwtSvid, or the leaf certificate's notAfter for
fetchX509Svid.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
},
"bodyTypes": {
"producerIn": { "carriers": [ "any" ], "description": "Only
validateJwtSvid reads the body: the token as text, when neither the
CamelSpiffeToken header nor an Authorization: Bearer header carries it. The
fetch operations ignore the body" },
@@ -49,10 +50,11 @@
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "Logical name of the endpoint" },
"audience": { "index": 1, "kind": "parameter", "displayName": "Audience",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The comma-separated audience(s) to request for
a JWT-SVID (fetchJwtSvid) or to validate against (validateJwtSvid). fetc [...]
"operation": { "index": 2, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType": "org.apache.camel.component.spiffe.SpiffeOperation",
"enum": [ "fetchX509Svid", "fetchJwtSvid", "validateJwtSvid" ], "deprecated":
false, "autowired": false, "secret": false, "defaultValue": "fetchX509Svid",
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "desc [...]
- "lazyStartProducer": { "index": 3, "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 [...]
- "workloadApiClient": { "index": 4, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does no [...]
- "allowOperationHeader": { "index": 5, "kind": "parameter", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
- "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tc [...]
- "x509Response": { "index": 7, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
+ "preserveBody": { "index": 3, "kind": "parameter", "displayName":
"Preserve Body", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether validateJwtSvid leaves the application
body untouched and publishes the verified identi [...]
+ "lazyStartProducer": { "index": 4, "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 [...]
+ "workloadApiClient": { "index": 5, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does no [...]
+ "allowOperationHeader": { "index": 6, "kind": "parameter", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
+ "spiffeSocketPath": { "index": 7, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tc [...]
+ "x509Response": { "index": 8, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
index a7ad55e2bb67..c5e571cc5a82 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
@@ -119,6 +119,141 @@ A missing token fails with `IllegalArgumentException`; a
rejected one (invalid,
with `io.spiffe.exception.JwtSvidException`. Catch both —
`onException(io.spiffe.exception.JwtSvidException.class,
IllegalArgumentException.class)` — to answer `401`.
+== Preserve an application body during JWT validation
+
+Set `preserveBody=true` on `validateJwtSvid` to validate a request without
replacing or reading its body. In this mode
+supply the token through `CamelSpiffeToken` or `Authorization: Bearer`; there
is no body-token fallback, including
+when the header is missing. The default remains `preserveBody=false`, which
returns the SDK `JwtSvid` in the body.
+
+After validation the `CamelSpiffeJwtIdentity` exchange property contains a
`SpiffeJwtIdentity` with getters for
+`spiffeId`, `expiry` (`Instant`), `audience` and `claims`. It contains no
bearer token. Claims are copied into
+unmodifiable maps/lists and omitted from `toString`, but can still contain
sensitive application data: select only
+the fields a downstream authorization policy needs. The identity property and
identity/expiry output headers are
+cleared before each validation, including one that fails.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("platform-http:/orders")
+
.to("spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true")
+ .removeHeaders("Authorization")
+ .setProperty("subject",
simple("${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}"))
+ .to("direct:handleOrder");
+----
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="platform-http:/orders"/>
+ <to
uri="spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true"/>
+ <removeHeaders pattern="Authorization"/>
+ <setProperty
name="subject"><simple>${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}</simple></setProperty>
+ <to uri="direct:handleOrder"/>
+</route>
+----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: platform-http:/orders
+ steps:
+ - to:
+ uri:
"spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true"
+ - removeHeaders:
+ pattern: Authorization
+ - setProperty:
+ name: subject
+ simple: "${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}"
+ - to:
+ uri: direct:handleOrder
+----
+====
+
+== Protect a route segment with a JWT-SVID policy
+
+`SpiffeJwtSecurityPolicy` combines audience validation with an explicit
identity rule, preserves the application body,
+and throws `CamelAuthorizationException` before the guarded processor runs for
missing, rejected or unauthorized
+tokens. Set `audience` and exactly one of `acceptedSpiffeIds` or
`acceptAnySpiffeId=true`. Audience and identity rules
+come from policy configuration; message headers cannot override them. The
policy reads tokens from the same two
+headers as body-preserving producer validation and never from the body.
+
+Each guarded processor owns an endpoint-managed Workload API client, released
when its route stops and recreated
+on restart. The policy starts from the configuration of the `spiffe`
component, so the `camel.component.spiffe.*`
+settings and a client autowired on the component apply to it; its own
`workloadApiClient` and `spiffeSocketPath`
+options take precedence, and `SPIFFE_ENDPOINT_SOCKET` is used when neither
names a client or a socket. An injected
+`workloadApiClient` remains owned by its supplier and is never closed by the
policy.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+SpiffeJwtSecurityPolicy jwtPolicy = new SpiffeJwtSecurityPolicy();
+jwtPolicy.setAudience("spiffe://example.org/orders");
+jwtPolicy.setAcceptedSpiffeIds("spiffe://example.org/frontend");
+getContext().getRegistry().bind("jwtPolicy", jwtPolicy);
+
+from("platform-http:/orders")
+ .policy(jwtPolicy)
+ .removeHeaders("Authorization")
+ .to("direct:handleOrder");
+----
+XML::
++
+[source,xml]
+----
+<bean id="jwtPolicy"
class="org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy">
+ <property name="audience" value="spiffe://example.org/orders"/>
+ <property name="acceptedSpiffeIds" value="spiffe://example.org/frontend"/>
+</bean>
+<route>
+ <from uri="platform-http:/orders"/>
+ <policy ref="jwtPolicy">
+ <removeHeaders pattern="Authorization"/>
+ <to uri="direct:handleOrder"/>
+ </policy>
+</route>
+----
+YAML::
++
+[source,yaml]
+----
+- beans:
+ - name: jwtPolicy
+ type: org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy
+ properties:
+ audience: spiffe://example.org/orders
+ acceptedSpiffeIds: spiffe://example.org/frontend
+- route:
+ from:
+ uri: platform-http:/orders
+ steps:
+ - policy:
+ ref: jwtPolicy
+ steps:
+ - removeHeaders:
+ pattern: Authorization
+ - to:
+ uri: direct:handleOrder
+----
+====
+
+The successful policy publishes the same token-free `CamelSpiffeJwtIdentity`
property, and nothing else: unlike the
+producer, it sets no `CamelSpiffeSpiffeId` or `CamelSpiffeExpiry` header, so
the identity is a value the route
+established, which a sender cannot put on the message, and it does not travel
to
+outbound producers. Check the property, not a header, further down the route.
Extract verified fields into
+exchange properties to compose with OPA or OpenFGA; for example, copy its
`spiffeId` into `subject`, use
+`includeProperties=subject` with OPA, or a configured OpenFGA `user`
expression based on that property. Choosing
+`acceptAnySpiffeId` asserts that audience validation is sufficient at this
step and leaves finer authorization to
+the downstream policy. Always remove credential headers before forwarding
requests to an unrelated destination.
+
== Mutual TLS with SPIFFE (SSLContextParameters)
For X.509-based zero-trust mTLS, the component provides
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
index 6f9c99a2e708..13d66f8a2f44 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
@@ -40,6 +40,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
case "operation":
getOrCreateConfiguration(target).setOperation(property(camelContext,
org.apache.camel.component.spiffe.SpiffeOperation.class, value)); return true;
+ case "preservebody":
+ case "preserveBody":
getOrCreateConfiguration(target).setPreserveBody(property(camelContext,
boolean.class, value)); return true;
case "spiffesocketpath":
case "spiffeSocketPath":
getOrCreateConfiguration(target).setSpiffeSocketPath(property(camelContext,
java.lang.String.class, value)); return true;
case "workloadapiclient":
@@ -67,6 +69,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
case "operation": return
org.apache.camel.component.spiffe.SpiffeOperation.class;
+ case "preservebody":
+ case "preserveBody": return boolean.class;
case "spiffesocketpath":
case "spiffeSocketPath": return java.lang.String.class;
case "workloadapiclient":
@@ -90,6 +94,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
case "operation": return
getOrCreateConfiguration(target).getOperation();
+ case "preservebody":
+ case "preserveBody": return
getOrCreateConfiguration(target).isPreserveBody();
case "spiffesocketpath":
case "spiffeSocketPath": return
getOrCreateConfiguration(target).getSpiffeSocketPath();
case "workloadapiclient":
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
index 5e2c3527b378..69273dc83252 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
@@ -30,6 +30,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
case "operation":
target.getConfiguration().setOperation(property(camelContext,
org.apache.camel.component.spiffe.SpiffeOperation.class, value)); return true;
+ case "preservebody":
+ case "preserveBody":
target.getConfiguration().setPreserveBody(property(camelContext, boolean.class,
value)); return true;
case "spiffesocketpath":
case "spiffeSocketPath":
target.getConfiguration().setSpiffeSocketPath(property(camelContext,
java.lang.String.class, value)); return true;
case "workloadapiclient":
@@ -54,6 +56,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
case "operation": return
org.apache.camel.component.spiffe.SpiffeOperation.class;
+ case "preservebody":
+ case "preserveBody": return boolean.class;
case "spiffesocketpath":
case "spiffeSocketPath": return java.lang.String.class;
case "workloadapiclient":
@@ -74,6 +78,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
case "operation": return target.getConfiguration().getOperation();
+ case "preservebody":
+ case "preserveBody": return target.getConfiguration().isPreserveBody();
case "spiffesocketpath":
case "spiffeSocketPath": return
target.getConfiguration().getSpiffeSocketPath();
case "workloadapiclient":
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
index 5af759af544b..86d4b10cd3aa 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
@@ -24,12 +24,13 @@ public class SpiffeEndpointUriFactory extends
org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(8);
+ Set<String> props = new HashSet<>(9);
props.add("allowOperationHeader");
props.add("audience");
props.add("label");
props.add("lazyStartProducer");
props.add("operation");
+ props.add("preserveBody");
props.add("spiffeSocketPath");
props.add("workloadApiClient");
props.add("x509Response");
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicyConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicyConfigurer.java
new file mode 100644
index 000000000000..0a3cc8d6f592
--- /dev/null
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicyConfigurer.java
@@ -0,0 +1,73 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.spiffe;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class SpiffeJwtSecurityPolicyConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy target =
(org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId":
target.setAcceptAnySpiffeId(property(camelContext, boolean.class, value));
return true;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds":
target.setAcceptedSpiffeIds(property(camelContext, java.lang.String.class,
value)); return true;
+ case "audience": target.setAudience(property(camelContext,
java.lang.String.class, value)); return true;
+ case "spiffesocketpath":
+ case "spiffeSocketPath":
target.setSpiffeSocketPath(property(camelContext, java.lang.String.class,
value)); return true;
+ case "workloadapiclient":
+ case "workloadApiClient":
target.setWorkloadApiClient(property(camelContext,
io.spiffe.workloadapi.WorkloadApiClient.class, value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return boolean.class;
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return java.lang.String.class;
+ case "audience": return java.lang.String.class;
+ case "spiffesocketpath":
+ case "spiffeSocketPath": return java.lang.String.class;
+ case "workloadapiclient":
+ case "workloadApiClient": return
io.spiffe.workloadapi.WorkloadApiClient.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy target =
(org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "acceptanyspiffeid":
+ case "acceptAnySpiffeId": return target.isAcceptAnySpiffeId();
+ case "acceptedspiffeids":
+ case "acceptedSpiffeIds": return target.getAcceptedSpiffeIds();
+ case "audience": return target.getAudience();
+ case "spiffesocketpath":
+ case "spiffeSocketPath": return target.getSpiffeSocketPath();
+ case "workloadapiclient":
+ case "workloadApiClient": return target.getWorkloadApiClient();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
b/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
index 9e4d228ec454..69434d105cbc 100644
---
a/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
+++
b/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
@@ -28,18 +28,19 @@
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"lazyStartProducer": { "index": 2, "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 [...]
"operation": { "index": 3, "kind": "property", "displayName": "Operation",
"group": "producer", "label": "", "required": false, "type": "enum",
"javaType": "org.apache.camel.component.spiffe.SpiffeOperation", "enum": [
"fetchX509Svid", "fetchJwtSvid", "validateJwtSvid" ], "deprecated": false,
"autowired": false, "secret": false, "defaultValue": "fetchX509Svid",
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "descr [...]
- "autowiredEnabled": { "index": 4, "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 [...]
- "workloadApiClient": { "index": 5, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does not [...]
- "allowOperationHeader": { "index": 6, "kind": "property", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
- "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tcp [...]
- "x509Response": { "index": 8, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
+ "preserveBody": { "index": 4, "kind": "property", "displayName": "Preserve
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether validateJwtSvid leaves the application
body untouched and publishes the verified identit [...]
+ "autowiredEnabled": { "index": 5, "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 [...]
+ "workloadApiClient": { "index": 6, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does not [...]
+ "allowOperationHeader": { "index": 7, "kind": "property", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
+ "spiffeSocketPath": { "index": 8, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tcp [...]
+ "x509Response": { "index": 9, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
},
"headers": {
"CamelSpiffeOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.spiffe.SpiffeOperation or String", "deprecated":
false, "autowired": false, "secret": false, "description": "Overrides the
operation to be used by the producer. Ignored unless the endpoint sets
allowOperationHeader=true, because the operation decides whether the endpoint
validates a token or mints one.", "const [...]
"CamelSpiffeAudience": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The comma-separated audience(s) for the fetchJwtSvid operation.
Ignored by validateJwtSvid, which always validates against the configured
audience: there the audience is the check that binds the token to this
workload, not a parameter.", "constantName": "or [...]
"CamelSpiffeToken": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The JWT-SVID token to validate, for the validateJwtSvid
operation.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#TOKEN" },
"CamelSpiffeSpiffeId": { "index": 3, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "autowired": false, "secret": false,
"description": "The SPIFFE ID of the returned SVID.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#SPIFFE_ID" },
- "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "autowired": false, "secret": false,
"description": "The expiry of the returned SVID: the token expiry for
fetchJwtSvid, or the leaf certificate's notAfter for fetchX509Svid.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
+ "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "autowired": false, "secret": false,
"description": "The expiry of the returned SVID: the token expiry for
fetchJwtSvid or validateJwtSvid, or the leaf certificate's notAfter for
fetchX509Svid.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
},
"bodyTypes": {
"producerIn": { "carriers": [ "any" ], "description": "Only
validateJwtSvid reads the body: the token as text, when neither the
CamelSpiffeToken header nor an Authorization: Bearer header carries it. The
fetch operations ignore the body" },
@@ -49,10 +50,11 @@
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "Logical name of the endpoint" },
"audience": { "index": 1, "kind": "parameter", "displayName": "Audience",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The comma-separated audience(s) to request for
a JWT-SVID (fetchJwtSvid) or to validate against (validateJwtSvid). fetc [...]
"operation": { "index": 2, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType": "org.apache.camel.component.spiffe.SpiffeOperation",
"enum": [ "fetchX509Svid", "fetchJwtSvid", "validateJwtSvid" ], "deprecated":
false, "autowired": false, "secret": false, "defaultValue": "fetchX509Svid",
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "desc [...]
- "lazyStartProducer": { "index": 3, "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 [...]
- "workloadApiClient": { "index": 4, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does no [...]
- "allowOperationHeader": { "index": 5, "kind": "parameter", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
- "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tc [...]
- "x509Response": { "index": 7, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
+ "preserveBody": { "index": 3, "kind": "parameter", "displayName":
"Preserve Body", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether validateJwtSvid leaves the application
body untouched and publishes the verified identi [...]
+ "lazyStartProducer": { "index": 4, "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 [...]
+ "workloadApiClient": { "index": 5, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "An existing WorkloadApiClient to use. When
set, the component does no [...]
+ "allowOperationHeader": { "index": 6, "kind": "parameter", "displayName":
"Allow Operation Header", "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.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
+ "spiffeSocketPath": { "index": 7, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example unix:\/\/\/tmp\/agent.sock or tc [...]
+ "x509Response": { "index": 8, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
}
}
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 094c2262df2c..9366d02e694c 100644
---
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-bean=SpiffeSSLContextParameters SpiffeSecurityPolicy
+bean=SpiffeJwtSecurityPolicy SpiffeSSLContextParameters SpiffeSecurityPolicy
groupId=org.apache.camel
artifactId=camel-spiffe
version=4.24.0-SNAPSHOT
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeJwtSecurityPolicy.json
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeJwtSecurityPolicy.json
new file mode 100644
index 000000000000..aa7360e6dcc6
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/bean/SpiffeJwtSecurityPolicy.json
@@ -0,0 +1,16 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "SpiffeJwtSecurityPolicy",
+ "javaType": "org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy",
+ "interfaceType": "org.apache.camel.spi.AuthorizationPolicy",
+ "title": "Spiffe Jwt Security Policy",
+ "description": "Validates a JWT-SVID and authorizes its SPIFFE identity
before a route segment, preserving the application body. A missing, rejected or
unauthorized token fails closed.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-spiffe",
+ "version": "4.24.0-SNAPSHOT",
+ "properties": { "audience": { "index": 0, "kind": "property",
"displayName": "Audience", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "Comma-separated audiences to validate against. A token is
accepted when it validates for any configured audience. Message headers cannot
override this check." }, "acceptedSpiffeIds": { "index": 1, "kind": "property",
"displayName": "Accepted Spiffe Ids", "r [...]
+ }
+}
+
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy
new file mode 100644
index 000000000000..f664a48e71a8
--- /dev/null
+++
b/components/camel-spiffe/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicyConfigurer
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index a7ad55e2bb67..c5e571cc5a82 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -119,6 +119,141 @@ A missing token fails with `IllegalArgumentException`; a
rejected one (invalid,
with `io.spiffe.exception.JwtSvidException`. Catch both —
`onException(io.spiffe.exception.JwtSvidException.class,
IllegalArgumentException.class)` — to answer `401`.
+== Preserve an application body during JWT validation
+
+Set `preserveBody=true` on `validateJwtSvid` to validate a request without
replacing or reading its body. In this mode
+supply the token through `CamelSpiffeToken` or `Authorization: Bearer`; there
is no body-token fallback, including
+when the header is missing. The default remains `preserveBody=false`, which
returns the SDK `JwtSvid` in the body.
+
+After validation the `CamelSpiffeJwtIdentity` exchange property contains a
`SpiffeJwtIdentity` with getters for
+`spiffeId`, `expiry` (`Instant`), `audience` and `claims`. It contains no
bearer token. Claims are copied into
+unmodifiable maps/lists and omitted from `toString`, but can still contain
sensitive application data: select only
+the fields a downstream authorization policy needs. The identity property and
identity/expiry output headers are
+cleared before each validation, including one that fails.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("platform-http:/orders")
+
.to("spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true")
+ .removeHeaders("Authorization")
+ .setProperty("subject",
simple("${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}"))
+ .to("direct:handleOrder");
+----
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="platform-http:/orders"/>
+ <to
uri="spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true"/>
+ <removeHeaders pattern="Authorization"/>
+ <setProperty
name="subject"><simple>${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}</simple></setProperty>
+ <to uri="direct:handleOrder"/>
+</route>
+----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: platform-http:/orders
+ steps:
+ - to:
+ uri:
"spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/orders&preserveBody=true"
+ - removeHeaders:
+ pattern: Authorization
+ - setProperty:
+ name: subject
+ simple: "${exchangeProperty.CamelSpiffeJwtIdentity.spiffeId}"
+ - to:
+ uri: direct:handleOrder
+----
+====
+
+== Protect a route segment with a JWT-SVID policy
+
+`SpiffeJwtSecurityPolicy` combines audience validation with an explicit
identity rule, preserves the application body,
+and throws `CamelAuthorizationException` before the guarded processor runs for
missing, rejected or unauthorized
+tokens. Set `audience` and exactly one of `acceptedSpiffeIds` or
`acceptAnySpiffeId=true`. Audience and identity rules
+come from policy configuration; message headers cannot override them. The
policy reads tokens from the same two
+headers as body-preserving producer validation and never from the body.
+
+Each guarded processor owns an endpoint-managed Workload API client, released
when its route stops and recreated
+on restart. The policy starts from the configuration of the `spiffe`
component, so the `camel.component.spiffe.*`
+settings and a client autowired on the component apply to it; its own
`workloadApiClient` and `spiffeSocketPath`
+options take precedence, and `SPIFFE_ENDPOINT_SOCKET` is used when neither
names a client or a socket. An injected
+`workloadApiClient` remains owned by its supplier and is never closed by the
policy.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+SpiffeJwtSecurityPolicy jwtPolicy = new SpiffeJwtSecurityPolicy();
+jwtPolicy.setAudience("spiffe://example.org/orders");
+jwtPolicy.setAcceptedSpiffeIds("spiffe://example.org/frontend");
+getContext().getRegistry().bind("jwtPolicy", jwtPolicy);
+
+from("platform-http:/orders")
+ .policy(jwtPolicy)
+ .removeHeaders("Authorization")
+ .to("direct:handleOrder");
+----
+XML::
++
+[source,xml]
+----
+<bean id="jwtPolicy"
class="org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy">
+ <property name="audience" value="spiffe://example.org/orders"/>
+ <property name="acceptedSpiffeIds" value="spiffe://example.org/frontend"/>
+</bean>
+<route>
+ <from uri="platform-http:/orders"/>
+ <policy ref="jwtPolicy">
+ <removeHeaders pattern="Authorization"/>
+ <to uri="direct:handleOrder"/>
+ </policy>
+</route>
+----
+YAML::
++
+[source,yaml]
+----
+- beans:
+ - name: jwtPolicy
+ type: org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy
+ properties:
+ audience: spiffe://example.org/orders
+ acceptedSpiffeIds: spiffe://example.org/frontend
+- route:
+ from:
+ uri: platform-http:/orders
+ steps:
+ - policy:
+ ref: jwtPolicy
+ steps:
+ - removeHeaders:
+ pattern: Authorization
+ - to:
+ uri: direct:handleOrder
+----
+====
+
+The successful policy publishes the same token-free `CamelSpiffeJwtIdentity`
property, and nothing else: unlike the
+producer, it sets no `CamelSpiffeSpiffeId` or `CamelSpiffeExpiry` header, so
the identity is a value the route
+established, which a sender cannot put on the message, and it does not travel
to
+outbound producers. Check the property, not a header, further down the route.
Extract verified fields into
+exchange properties to compose with OPA or OpenFGA; for example, copy its
`spiffeId` into `subject`, use
+`includeProperties=subject` with OPA, or a configured OpenFGA `user`
expression based on that property. Choosing
+`acceptAnySpiffeId` asserts that audience validation is sufficient at this
step and leaves finer authorization to
+the downstream policy. Always remove credential headers before forwarding
requests to an unrelated destination.
+
== Mutual TLS with SPIFFE (SSLContextParameters)
For X.509-based zero-trust mTLS, the component provides
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
index 98fc0e053286..57e4bc99fea5 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
@@ -37,6 +37,9 @@ public class SpiffeConfiguration implements Cloneable {
@UriParam
private String audience;
+ @UriParam(defaultValue = "false")
+ private boolean preserveBody;
+
@UriParam(label = "security", defaultValue = "false", security =
"insecure:dev")
private boolean allowOperationHeader;
@@ -99,6 +102,19 @@ public class SpiffeConfiguration implements Cloneable {
this.audience = audience;
}
+ public boolean isPreserveBody() {
+ return preserveBody;
+ }
+
+ /**
+ * Whether validateJwtSvid leaves the application body untouched and
publishes the verified identity through the
+ * CamelSpiffeJwtIdentity exchange property. In this mode the token must
be in CamelSpiffeToken or an Authorization
+ * Bearer header: the body is never read as a token. The default returns
the JwtSvid in the body as before.
+ */
+ public void setPreserveBody(boolean preserveBody) {
+ this.preserveBody = preserveBody;
+ }
+
public boolean isAllowOperationHeader() {
return allowOperationHeader;
}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
index 48bc80e91412..6a3fb689b841 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
@@ -43,7 +43,7 @@ public final class SpiffeConstants {
public static final String SPIFFE_ID = HEADER_PREFIX + "SpiffeId";
@Metadata(label = "producer",
- description = "The expiry of the returned SVID: the token expiry
for fetchJwtSvid, or the leaf"
+ description = "The expiry of the returned SVID: the token expiry
for fetchJwtSvid or validateJwtSvid, or the leaf"
+ " certificate's notAfter for fetchX509Svid.",
javaType = "java.util.Date")
public static final String EXPIRY = HEADER_PREFIX + "Expiry";
@@ -55,6 +55,12 @@ public final class SpiffeConstants {
*/
public static final String PEER_SPIFFE_ID = HEADER_PREFIX + "PeerId";
+ /**
+ * Exchange property carrying the token-free verified JWT identity and
claims as a {@link SpiffeJwtIdentity}.
+ * Cleared before each validation and populated only after successful
validation and, for a policy, authorization.
+ */
+ public static final String JWT_IDENTITY = HEADER_PREFIX + "JwtIdentity";
+
private SpiffeConstants() {
}
}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtIdentity.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtIdentity.java
new file mode 100644
index 000000000000..6d59639e3a4a
--- /dev/null
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtIdentity.java
@@ -0,0 +1,90 @@
+/*
+ * 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.spiffe;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import io.spiffe.svid.jwtsvid.JwtSvid;
+
+/**
+ * A snapshot of a validated JWT-SVID's identity and claims, without the
bearer token. Claims may still contain
+ * application-sensitive information; they are deliberately omitted from
{@link #toString()}.
+ */
+public final class SpiffeJwtIdentity {
+ private final String spiffeId;
+ private final Instant expiry;
+ private final Set<String> audience;
+ private final Map<String, Object> claims;
+
+ SpiffeJwtIdentity(JwtSvid svid) {
+ spiffeId = svid.getSpiffeId().toString();
+ expiry = svid.getExpiry() != null ? svid.getExpiry().toInstant() :
null;
+ audience = svid.getAudience() != null ? Set.copyOf(svid.getAudience())
: Set.of();
+ claims = svid.getClaims() != null ? copyClaims(svid.getClaims()) :
Map.of();
+ }
+
+ public String getSpiffeId() {
+ return spiffeId;
+ }
+
+ public Instant getExpiry() {
+ return expiry;
+ }
+
+ public Set<String> getAudience() {
+ return audience;
+ }
+
+ public Map<String, Object> getClaims() {
+ return claims;
+ }
+
+ private static Map<String, Object> copyClaims(Map<String, Object> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((key, value) -> copy.put(key, snapshot(value)));
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object snapshot(Object value) {
+ if (value instanceof Map<?, ?> values) {
+ Map<Object, Object> copy = new LinkedHashMap<>();
+ values.forEach((key, item) -> copy.put(key, snapshot(item)));
+ return Collections.unmodifiableMap(copy);
+ }
+ if (value instanceof List<?> values) {
+ List<Object> copy = new ArrayList<>(values.size());
+ values.forEach(item -> copy.add(snapshot(item)));
+ return Collections.unmodifiableList(copy);
+ }
+ if (value instanceof Date date) {
+ return date.toInstant();
+ }
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "SpiffeJwtIdentity[spiffeId=" + spiffeId + ", expiry=" + expiry
+ "]";
+ }
+}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicy.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicy.java
new file mode 100644
index 000000000000..52e77aa53711
--- /dev/null
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityPolicy.java
@@ -0,0 +1,144 @@
+/*
+ * 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.spiffe;
+
+import java.util.Set;
+
+import io.spiffe.spiffeid.SpiffeId;
+import io.spiffe.workloadapi.WorkloadApiClient;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Validates an incoming JWT-SVID for configured audiences and authorizes its
SPIFFE identity before a route segment.
+ * The application body is never read as a token or replaced. The verified,
token-free identity and claims are exposed
+ * as the {@link SpiffeConstants#JWT_IDENTITY} exchange property.
+ */
+@Metadata(label = "bean",
+ description = "Validates a JWT-SVID and authorizes its SPIFFE
identity before a route segment, preserving the"
+ + " application body. A missing, rejected or
unauthorized token fails closed.",
+ annotations = {
"interfaceName=org.apache.camel.spi.AuthorizationPolicy" })
+@Configurer(metadataOnly = true)
+public class SpiffeJwtSecurityPolicy implements AuthorizationPolicy {
+ @Metadata(required = true,
+ description = "Comma-separated audiences to validate against. A
token is accepted when it validates for"
+ + " any configured audience. Message headers
cannot override this check.")
+ private String audience;
+ @Metadata(description = "Comma-separated allow-list of validated SPIFFE
identities to authorize. Set exactly one"
+ + " of acceptedSpiffeIds and acceptAnySpiffeId.")
+ private String acceptedSpiffeIds;
+ @Metadata(defaultValue = "false",
+ description = "Authorize any SPIFFE identity whose JWT-SVID
validates for a configured audience, leaving"
+ + " further authorization to a downstream policy.
Mutually exclusive with acceptedSpiffeIds.")
+ private boolean acceptAnySpiffeId;
+ @Metadata(label = "security",
+ description = "Path to the Workload API endpoint. Defaults to
the spiffeSocketPath of the spiffe component,"
+ + " then to the SPIFFE_ENDPOINT_SOCKET environment
variable. Ignored when a"
+ + " workloadApiClient is used.")
+ private String spiffeSocketPath;
+ @Metadata(label = "advanced",
+ description = "An existing WorkloadApiClient to use. Defaults to
the client of the spiffe component, when"
+ + " it has one. The client remains owned by its
supplier and is not closed when the guarded"
+ + " route stops.")
+ private WorkloadApiClient workloadApiClient;
+
+ private volatile Set<SpiffeId> acceptedSet = Set.of();
+
+ @Override
+ public void beforeWrap(Route route, NamedNode definition) {
+ SpiffeJwtValidator.audiences(audience, "set the audience option");
+ boolean haveAllowList = ObjectHelper.isNotEmpty(acceptedSpiffeIds);
+ if (acceptAnySpiffeId == haveAllowList) {
+ throw new IllegalStateException(
+ "SpiffeJwtSecurityPolicy requires exactly one of
acceptedSpiffeIds and acceptAnySpiffeId");
+ }
+ Set<SpiffeId> parsed = haveAllowList ?
SpiffeSecurityPolicy.parseSpiffeIds(acceptedSpiffeIds) : Set.of();
+ if (!acceptAnySpiffeId && parsed.isEmpty()) {
+ throw new IllegalStateException("acceptedSpiffeIds did not contain
any SPIFFE ID after trimming");
+ }
+ acceptedSet = parsed;
+ }
+
+ @Override
+ public Processor wrap(Route route, Processor processor) {
+ // Start from the configuration of the spiffe component, so that its
camel.component.spiffe.* settings and a
+ // client autowired on it apply to the policy as well; what is set on
the policy itself takes precedence
+ SpiffeComponent component =
route.getCamelContext().getComponent("spiffe", SpiffeComponent.class);
+ SpiffeConfiguration configuration = component.getConfiguration() !=
null
+ ? component.getConfiguration().copy() : new
SpiffeConfiguration();
+ configuration.setOperation(SpiffeOperation.validateJwtSvid);
+ configuration.setAudience(audience);
+ configuration.setPreserveBody(true);
+ if (spiffeSocketPath != null) {
+ configuration.setSpiffeSocketPath(spiffeSocketPath);
+ }
+ if (workloadApiClient != null) {
+ configuration.setWorkloadApiClient(workloadApiClient);
+ }
+ SpiffeEndpoint endpoint = new SpiffeEndpoint("spiffe:jwt-policy",
component, configuration);
+ return new SpiffeJwtSecurityProcessor(processor, this, endpoint);
+ }
+
+ boolean isAuthorized(SpiffeId id) {
+ return acceptAnySpiffeId || acceptedSet.contains(id);
+ }
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public void setAudience(String audience) {
+ this.audience = audience;
+ }
+
+ public String getAcceptedSpiffeIds() {
+ return acceptedSpiffeIds;
+ }
+
+ public void setAcceptedSpiffeIds(String acceptedSpiffeIds) {
+ this.acceptedSpiffeIds = acceptedSpiffeIds;
+ }
+
+ public boolean isAcceptAnySpiffeId() {
+ return acceptAnySpiffeId;
+ }
+
+ public void setAcceptAnySpiffeId(boolean acceptAnySpiffeId) {
+ this.acceptAnySpiffeId = acceptAnySpiffeId;
+ }
+
+ public String getSpiffeSocketPath() {
+ return spiffeSocketPath;
+ }
+
+ public void setSpiffeSocketPath(String spiffeSocketPath) {
+ this.spiffeSocketPath = spiffeSocketPath;
+ }
+
+ public WorkloadApiClient getWorkloadApiClient() {
+ return workloadApiClient;
+ }
+
+ public void setWorkloadApiClient(WorkloadApiClient workloadApiClient) {
+ this.workloadApiClient = workloadApiClient;
+ }
+}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityProcessor.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityProcessor.java
new file mode 100644
index 000000000000..f38dad28c227
--- /dev/null
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtSecurityProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * 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.spiffe;
+
+import io.spiffe.exception.JwtSvidException;
+import io.spiffe.svid.jwtsvid.JwtSvid;
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.support.processor.DelegateAsyncProcessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Keeps JWT validation and its Workload API client within the guarded
route's lifecycle. */
+final class SpiffeJwtSecurityProcessor extends DelegateAsyncProcessor {
+ private static final Logger LOG =
LoggerFactory.getLogger(SpiffeJwtSecurityProcessor.class);
+
+ private final SpiffeJwtSecurityPolicy policy;
+ private final SpiffeEndpoint endpoint;
+
+ SpiffeJwtSecurityProcessor(Processor processor, SpiffeJwtSecurityPolicy
policy, SpiffeEndpoint endpoint) {
+ super(processor);
+ this.policy = policy;
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ endpoint.start();
+ try {
+ super.doStart();
+ } catch (Exception e) {
+ endpoint.stop();
+ throw e;
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ try {
+ super.doStop();
+ } finally {
+ endpoint.stop();
+ }
+ }
+
+ @Override
+ public boolean process(Exchange exchange, AsyncCallback callback) {
+ try {
+ JwtSvid svid = SpiffeJwtValidator.validate(
+ endpoint.getWorkloadApiClient(), exchange,
endpoint.getConfiguration().getAudience(), false);
+ if (!policy.isAuthorized(svid.getSpiffeId())) {
+ throw new IllegalArgumentException("Validated SPIFFE identity
is not authorized by this policy");
+ }
+ SpiffeJwtValidator.recordIdentity(exchange, svid, false);
+ } catch (Exception e) {
+ // Only the class of the failure is logged: the message of the SDK
exception may contain the credential.
+ // A failure that is not a verdict on the token, such as the
Workload API being unreachable, is worth a
+ // warning, since to the caller every failure is the same 401 and
an outage would look like a 401 storm.
+ if (e instanceof JwtSvidException || e instanceof
IllegalArgumentException) {
+ LOG.debug("JWT-SVID authentication or authorization failed:
{}", e.getClass().getName());
+ } else {
+ LOG.warn("JWT-SVID validation could not be performed ({}): is
the Workload API reachable?",
+ e.getClass().getName());
+ }
+ SpiffeJwtValidator.clearIdentity(exchange);
+
exchange.getMessage().setHeader(Exchange.AUTHENTICATION_FAILURE_POLICY_ID,
policy.getClass().getSimpleName());
+ // Do not include the SDK exception message: it may contain the
presented credential.
+ exchange.setException(new CamelAuthorizationException("JWT-SVID
authentication or authorization failed", exchange));
+ callback.done(true);
+ return true;
+ }
+ return super.process(exchange, callback);
+ }
+}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtValidator.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtValidator.java
new file mode 100644
index 000000000000..efeb28ba5d1b
--- /dev/null
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeJwtValidator.java
@@ -0,0 +1,129 @@
+/*
+ * 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.spiffe;
+
+import java.util.Arrays;
+
+import io.spiffe.exception.JwtSvidException;
+import io.spiffe.svid.jwtsvid.JwtSvid;
+import io.spiffe.workloadapi.WorkloadApiClient;
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.util.ObjectHelper;
+
+/** Shared JWT validation for the producer and the route security policy. */
+final class SpiffeJwtValidator {
+ private SpiffeJwtValidator() {
+ }
+
+ static JwtSvid validate(WorkloadApiClient client, Exchange exchange,
String configuredAudience, boolean allowBodyToken)
+ throws JwtSvidException {
+ clearIdentity(exchange);
+ Message message = exchange.getMessage();
+ String token = message.getHeader(SpiffeConstants.TOKEN, String.class);
+ if (ObjectHelper.isEmpty(token)) {
+ // a JWT-SVID is presented over HTTP as "Authorization: Bearer
<token>", so an HTTP route can validate it
+ // without a bean to strip the scheme; it is checked before the
body so that a request payload on a POST
+ // or PUT is not mistaken for the token
+ token = bearerToken(message.getHeader("Authorization",
String.class));
+ }
+ if (ObjectHelper.isEmpty(token) && allowBodyToken) {
+ token = message.getBody(String.class);
+ }
+ if (ObjectHelper.isEmpty(token)) {
+ throw new IllegalArgumentException(
+ "A JWT-SVID token is required (set CamelSpiffeToken or an
Authorization: Bearer header"
+ + (allowBodyToken ? ", or the
message body)" : ")"));
+ }
+ // The audience is the check here, not a parameter: it is what binds
the token to THIS workload, so it comes
+ // from the configuration only. Honouring a CamelSpiffeAudience header
would let a caller validate a token
+ // minted for someone else against an audience of their choosing.
+ //
+ // The Workload API validates against one audience at a time, so a
configured list is tried in turn and the
+ // token is accepted when any audience matches. Taking only the first
would silently enforce a narrower rule
+ // than the configuration asks for, which is the wrong failure mode
for a check that decides whether a caller
+ // is authenticated.
+ JwtSvidException failure = null;
+ for (String audience : audiences(configuredAudience, "set the audience
option")) {
+ try {
+ return client.validateJwtSvid(token, audience);
+ } catch (JwtSvidException e) {
+ // could be this audience, or the token itself; only once
every audience has failed do we know
+ failure = e;
+ }
+ }
+ // audiences never returns an empty array, so the loop ran and failure
is set; be explicit rather than leaving
+ // a reader (or a static analyser) to prove it
+ if (failure == null) {
+ throw new IllegalStateException("No configured audience was
evaluated");
+ }
+ throw failure;
+ }
+
+ /**
+ * Splits a comma-separated audience list, trimming each entry and
dropping blank ones.
+ *
+ * @param configured the configured list
+ * @param how how to configure it, for the error when it is empty
+ * @return the audiences, never empty
+ */
+ static String[] audiences(String configured, String how) {
+ if (ObjectHelper.isEmpty(configured)) {
+ throw new IllegalArgumentException("At least one audience is
required (" + how + ")");
+ }
+ String[] audiences =
Arrays.stream(configured.split(",")).map(String::trim).filter(s -> !s.isEmpty())
+ .toArray(String[]::new);
+ if (audiences.length == 0) {
+ throw new IllegalArgumentException("At least one non-blank
audience is required (" + how + ")");
+ }
+ return audiences;
+ }
+
+ static void clearIdentity(Exchange exchange) {
+ exchange.removeProperty(SpiffeConstants.JWT_IDENTITY);
+ exchange.getMessage().removeHeader(SpiffeConstants.SPIFFE_ID);
+ exchange.getMessage().removeHeader(SpiffeConstants.EXPIRY);
+ }
+
+ /**
+ * Publishes the verified identity. The exchange property is the value the
route established, which a sender cannot
+ * put on the message, and the one to check further down the route. The
producer also sets the identity headers, its
+ * documented output; the policy does not, so that the identity does not
travel in two channels, nor to outbound
+ * producers.
+ */
+ static void recordIdentity(Exchange exchange, JwtSvid svid, boolean
headers) {
+ SpiffeJwtIdentity identity = new SpiffeJwtIdentity(svid);
+ exchange.setProperty(SpiffeConstants.JWT_IDENTITY, identity);
+ if (headers) {
+ exchange.getMessage().setHeader(SpiffeConstants.SPIFFE_ID,
identity.getSpiffeId());
+ exchange.getMessage().setHeader(SpiffeConstants.EXPIRY,
svid.getExpiry());
+ }
+ }
+
+ private static String bearerToken(String authorization) {
+ if (ObjectHelper.isEmpty(authorization)) {
+ return null;
+ }
+ String value = authorization.trim();
+ String scheme = "Bearer ";
+ if (value.length() > scheme.length() && value.regionMatches(true, 0,
scheme, 0, scheme.length())) {
+ String token = value.substring(scheme.length()).trim();
+ return token.isEmpty() ? null : token;
+ }
+ return null;
+ }
+}
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
index 25ecd8632109..5018738e2502 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
@@ -18,7 +18,6 @@ package org.apache.camel.component.spiffe;
import java.util.Arrays;
-import io.spiffe.exception.JwtSvidException;
import io.spiffe.svid.jwtsvid.JwtSvid;
import io.spiffe.svid.x509svid.X509Svid;
import io.spiffe.workloadapi.WorkloadApiClient;
@@ -79,72 +78,13 @@ public class SpiffeProducer extends DefaultProducer {
}
private void validateJwtSvid(WorkloadApiClient client, Exchange exchange)
throws Exception {
- String token = exchange.getIn().getHeader(SpiffeConstants.TOKEN,
String.class);
- if (ObjectHelper.isEmpty(token)) {
- // a JWT-SVID is presented over HTTP as "Authorization: Bearer
<token>", so an HTTP route can validate it
- // without a bean to strip the scheme; check it before the body so
a request payload on a POST/PUT is not
- // mistaken for the token
- token = bearerToken(exchange.getIn().getHeader("Authorization",
String.class));
+ boolean preserveBody =
getEndpoint().getConfiguration().isPreserveBody();
+ JwtSvid svid = SpiffeJwtValidator.validate(
+ client, exchange,
getEndpoint().getConfiguration().getAudience(), !preserveBody);
+ SpiffeJwtValidator.recordIdentity(exchange, svid, true);
+ if (!preserveBody) {
+ getMessageForResponse(exchange).setBody(svid);
}
- if (ObjectHelper.isEmpty(token)) {
- token = exchange.getIn().getBody(String.class);
- }
- if (ObjectHelper.isEmpty(token)) {
- throw new IllegalArgumentException(
- "A JWT-SVID token is required for validateJwtSvid (set the
CamelSpiffeToken header, an"
- + " Authorization: Bearer
header, or the message body)");
- }
- // the audience is the check here, not a parameter: it is what binds
the token to THIS workload, so it
- // comes from the configuration only. Honouring CamelSpiffeAudience
would let a caller validate a token
- // minted for someone else against an audience of their choosing.
- JwtSvid svid = validateAgainstAnyAudience(client, token,
resolveConfiguredAudiences());
- Message message = getMessageForResponse(exchange);
- message.setBody(svid);
- message.setHeader(SpiffeConstants.SPIFFE_ID,
svid.getSpiffeId().toString());
- }
-
- /**
- * Validates the token against the configured audiences, accepting it if
<em>any</em> of them matches.
- * <p/>
- * The Workload API validates against one audience at a time, so a
configured list has to be tried in turn. Taking
- * only the first would silently enforce a narrower rule than the
configuration asks for, which is the wrong failure
- * mode for a check that decides whether a caller is authenticated.
- */
- private JwtSvid validateAgainstAnyAudience(WorkloadApiClient client,
String token, String[] audiences)
- throws JwtSvidException {
- JwtSvidException failure = null;
- for (String audience : audiences) {
- try {
- return client.validateJwtSvid(token, audience);
- } catch (JwtSvidException e) {
- // could be this audience, or the token itself; only once
every audience has failed do we know
- failure = e;
- }
- }
- // resolveConfiguredAudiences never returns an empty array, so the
loop ran and failure is set; be explicit
- // rather than leaving a reader (or a static analyser) to prove it
- if (failure == null) {
- throw new IllegalStateException("No audience was configured to
validate against");
- }
- throw failure;
- }
-
- /**
- * Extracts the token from an {@code Authorization: Bearer <token>} value,
matching the scheme case-insensitively
- * and trimming the token. Returns {@code null} for a missing, empty or
non-bearer value, so such a request falls
- * through to the single "token required" error rather than a
scheme-specific one.
- */
- private static String bearerToken(String authorization) {
- if (ObjectHelper.isEmpty(authorization)) {
- return null;
- }
- String value = authorization.trim();
- String scheme = "Bearer ";
- if (value.length() > scheme.length() && value.regionMatches(true, 0,
scheme, 0, scheme.length())) {
- String token = value.substring(scheme.length()).trim();
- return token.isEmpty() ? null : token;
- }
- return null;
}
private SpiffeOperation determineOperation(Exchange exchange) {
@@ -168,26 +108,8 @@ public class SpiffeProducer extends DefaultProducer {
return splitAudiences(audience, "set the audience option or the
CamelSpiffeAudience header");
}
- /**
- * Audience for a validation: taken from the configuration only, never
from the message.
- */
- private String[] resolveConfiguredAudiences() {
- // deliberately does not mention the header: it is ignored for
validation
- return splitAudiences(getEndpoint().getConfiguration().getAudience(),
"set the audience option");
- }
-
- private String[] splitAudiences(String audience, String how) {
- if (ObjectHelper.isEmpty(audience)) {
- throw new IllegalArgumentException("At least one audience is
required (" + how + ")");
- }
- String[] parts = Arrays.stream(audience.split(","))
- .map(String::trim)
- .filter(s -> !s.isEmpty())
- .toArray(String[]::new);
- if (parts.length == 0) {
- throw new IllegalArgumentException("At least one non-blank
audience is required (" + how + ")");
- }
- return parts;
+ private static String[] splitAudiences(String audience, String how) {
+ return SpiffeJwtValidator.audiences(audience, how);
}
private static Message getMessageForResponse(Exchange exchange) {
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeJwtValidationTest.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeJwtValidationTest.java
new file mode 100644
index 000000000000..d628dba5d572
--- /dev/null
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeJwtValidationTest.java
@@ -0,0 +1,316 @@
+/*
+ * 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.spiffe;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.spiffe.exception.JwtSvidException;
+import io.spiffe.spiffeid.SpiffeId;
+import io.spiffe.svid.jwtsvid.JwtSvid;
+import io.spiffe.workloadapi.DefaultWorkloadApiClient;
+import io.spiffe.workloadapi.WorkloadApiClient;
+import org.apache.camel.AsyncProcessor;
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.service.ServiceHelper;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class SpiffeJwtValidationTest {
+ private static final String CALLER = "spiffe://example.org/caller";
+ private static final String AUDIENCE = "spiffe://example.org/api";
+ private static final String TOKEN = "dummy-token";
+
+ @Test
+ void preservesAPojoAndPublishesATokenFreeIdentitySnapshot() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ JwtSvid svid = svid(CALLER);
+ List<String> roles = new ArrayList<>(List.of("reader"));
+ Map<String, Object> claims = new LinkedHashMap<>();
+ claims.put("roles", roles);
+ when(svid.getClaims()).thenReturn(claims);
+ when(svid.getToken()).thenReturn("private-token-value");
+ when(client.validateJwtSvid(TOKEN, AUDIENCE)).thenReturn(svid);
+ Object body = new Object();
+ Exchange exchange = exchange(context, body, true);
+ producer(context, client, true, AUDIENCE).process(exchange);
+
+ SpiffeJwtIdentity identity =
exchange.getProperty(SpiffeConstants.JWT_IDENTITY, SpiffeJwtIdentity.class);
+ assertThat(exchange.getMessage().getBody()).isSameAs(body);
+ assertThat(identity.getSpiffeId()).isEqualTo(CALLER);
+ assertThat(identity.getAudience()).containsExactly(AUDIENCE);
+
assertThat(identity.getExpiry()).isEqualTo(svid.getExpiry().toInstant());
+ roles.add("writer");
+ claims.put("extra", "secret");
+ assertThat(identity.getClaims()).containsOnlyKeys("roles");
+
assertThat(identity.getClaims().get("roles")).isEqualTo(List.of("reader"));
+ assertThat(identity.toString()).doesNotContain("reader", "roles",
"private-token-value");
+ assertThatThrownBy(() -> identity.getClaims().put("extra",
"value"))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ void preservesAStreamWithAndWithoutAnAuthorizationHeader() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ doReturn(svid(CALLER)).when(client).validateJwtSvid(TOKEN,
AUDIENCE);
+ byte[] payload = "application
payload".getBytes(StandardCharsets.UTF_8);
+ ByteArrayInputStream body = new ByteArrayInputStream(payload);
+ Exchange exchange = exchange(context, body, true);
+ SpiffeProducer producer = producer(context, client, true,
AUDIENCE);
+ producer.process(exchange);
+ assertThat(exchange.getMessage().getBody()).isSameAs(body);
+ assertThat(body.available()).isEqualTo(payload.length);
+
+ exchange.getMessage().removeHeader("Authorization");
+ assertThatThrownBy(() ->
producer.process(exchange)).isInstanceOf(IllegalArgumentException.class);
+ assertThat(exchange.getMessage().getBody()).isSameAs(body);
+ assertThat(body.available()).isEqualTo(payload.length);
+
assertThat(exchange.getProperty(SpiffeConstants.JWT_IDENTITY)).isNull();
+
assertThat(exchange.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isNull();
+ }
+ }
+
+ @Test
+ void retainsTheExistingDefaultBodyResponseAndTokenBodyFallback() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ JwtSvid svid = svid(CALLER);
+ when(client.validateJwtSvid(TOKEN, AUDIENCE)).thenReturn(svid);
+ Exchange exchange = exchange(context, TOKEN, false);
+ producer(context, client, false, AUDIENCE).process(exchange);
+ assertThat(exchange.getMessage().getBody()).isSameAs(svid);
+ assertThat(exchange.getProperty(SpiffeConstants.JWT_IDENTITY,
SpiffeJwtIdentity.class).getSpiffeId())
+ .isEqualTo(CALLER);
+ }
+ }
+
+ @Test
+ void rejectedTokensClearEarlierVerifiedOutputsWithoutChangingTheBody()
throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ SpiffeProducer producer = producer(context, client, true,
AUDIENCE);
+ for (String reason : List.of("invalid signature", "expired token",
"wrong audience")) {
+ doThrow(new
JwtSvidException(reason)).when(client).validateJwtSvid(TOKEN, AUDIENCE);
+ Object body = new Object();
+ Exchange exchange = exchange(context, body, true);
+ exchange.setProperty(SpiffeConstants.JWT_IDENTITY, "stale");
+ exchange.getMessage().setHeader(SpiffeConstants.SPIFFE_ID,
CALLER);
+ exchange.getMessage().setHeader(SpiffeConstants.EXPIRY, new
Date());
+ assertThatThrownBy(() ->
producer.process(exchange)).isInstanceOf(JwtSvidException.class);
+ assertThat(exchange.getMessage().getBody()).isSameAs(body);
+
assertThat(exchange.getProperty(SpiffeConstants.JWT_IDENTITY)).isNull();
+
assertThat(exchange.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isNull();
+
assertThat(exchange.getMessage().getHeader(SpiffeConstants.EXPIRY)).isNull();
+ }
+ }
+ }
+
+ @Test
+ void triesConfiguredAudiencesAndIgnoresTheMessageAudience() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ when(client.validateJwtSvid(TOKEN, "first")).thenThrow(new
JwtSvidException("wrong audience"));
+ doReturn(svid(CALLER)).when(client).validateJwtSvid(TOKEN,
AUDIENCE);
+ Exchange exchange = exchange(context, "payload", true);
+ exchange.getMessage().setHeader(SpiffeConstants.AUDIENCE,
"message-selected-audience");
+ producer(context, client, true, "first, " +
AUDIENCE).process(exchange);
+ verify(client).validateJwtSvid(TOKEN, "first");
+ verify(client).validateJwtSvid(TOKEN, AUDIENCE);
+ verify(client, never()).validateJwtSvid(TOKEN,
"message-selected-audience");
+ assertThat(exchange.getMessage().getBody()).isEqualTo("payload");
+ }
+ }
+
+ @Test
+ void policyPreservesBodyAndStopsTheGuardedProcessorOnEveryDenial() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ SpiffeJwtSecurityPolicy policy = new SpiffeJwtSecurityPolicy();
+ policy.setAudience(AUDIENCE);
+ policy.setAcceptedSpiffeIds(CALLER);
+ policy.setWorkloadApiClient(client);
+ Route route = mock(Route.class);
+ when(route.getCamelContext()).thenReturn(context);
+ policy.beforeWrap(route, null);
+ AtomicInteger received = new AtomicInteger();
+ Processor wrapped = policy.wrap(route, e ->
received.incrementAndGet());
+ ServiceHelper.startService(wrapped);
+ try {
+ doReturn(svid(CALLER)).when(client).validateJwtSvid(TOKEN,
AUDIENCE);
+ Exchange allowed = exchange(context, "payload", true);
+ ((AsyncProcessor) wrapped).process(allowed, done -> {
+ });
+ assertThat(received).hasValue(1);
+
assertThat(allowed.getMessage().getBody()).isEqualTo("payload");
+
assertThat(allowed.getProperty(SpiffeConstants.JWT_IDENTITY)).isInstanceOf(SpiffeJwtIdentity.class);
+ // the policy publishes the identity as a property only,
unlike the producer
+
assertThat(allowed.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isNull();
+
assertThat(allowed.getMessage().getHeader(SpiffeConstants.EXPIRY)).isNull();
+
+ for (String reason : List.of("missing", "unlisted", "invalid",
"expired", "wrong-audience")) {
+ Exchange denied = exchange(context, "payload",
!reason.equals("missing"));
+ denied.setProperty(SpiffeConstants.JWT_IDENTITY, "stale");
+ if (reason.equals("unlisted")) {
+
doReturn(svid("spiffe://example.org/other")).when(client).validateJwtSvid(TOKEN,
AUDIENCE);
+ } else if (!reason.equals("missing")) {
+ doThrow(new
JwtSvidException(reason)).when(client).validateJwtSvid(TOKEN, AUDIENCE);
+ }
+ ((AsyncProcessor) wrapped).process(denied, done -> {
+ });
+
assertThat(denied.getException()).isInstanceOf(CamelAuthorizationException.class);
+
assertThat(denied.getProperty(SpiffeConstants.JWT_IDENTITY)).isNull();
+
assertThat(denied.getMessage().getBody()).isEqualTo("payload");
+ assertThat(received).hasValue(1);
+ }
+ } finally {
+ ServiceHelper.stopService(wrapped);
+ }
+ verify(client, never()).close();
+ }
+ }
+
+ @Test
+ void policyUsesTheClientOfTheComponentWhenItSetsNone() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ WorkloadApiClient client = mock(WorkloadApiClient.class);
+ doReturn(svid(CALLER)).when(client).validateJwtSvid(TOKEN,
AUDIENCE);
+ // the client is configured on the component, as
camel.component.spiffe.workload-api-client or autowiring
+ // would do, and the policy names none
+ SpiffeConfiguration componentConfiguration = new
SpiffeConfiguration();
+ componentConfiguration.setWorkloadApiClient(client);
+ context.getComponent("spiffe",
SpiffeComponent.class).setConfiguration(componentConfiguration);
+
+ SpiffeJwtSecurityPolicy policy = new SpiffeJwtSecurityPolicy();
+ policy.setAudience(AUDIENCE);
+ policy.setAcceptedSpiffeIds(CALLER);
+ Route route = mock(Route.class);
+ when(route.getCamelContext()).thenReturn(context);
+ policy.beforeWrap(route, null);
+ AtomicInteger received = new AtomicInteger();
+ Processor wrapped = policy.wrap(route, e ->
received.incrementAndGet());
+ ServiceHelper.startService(wrapped);
+ try {
+ Exchange allowed = exchange(context, "payload", true);
+ ((AsyncProcessor) wrapped).process(allowed, done -> {
+ });
+ assertThat(allowed.getException()).isNull();
+ assertThat(received).hasValue(1);
+
assertThat(allowed.getProperty(SpiffeConstants.JWT_IDENTITY)).isInstanceOf(SpiffeJwtIdentity.class);
+ verify(client).validateJwtSvid(TOKEN, AUDIENCE);
+ } finally {
+ ServiceHelper.stopService(wrapped);
+ }
+ // the component's client is not the policy's to close
+ verify(client, never()).close();
+ }
+ }
+
+ @Test
+ void policyRequiresAudienceAndAnExplicitIdentityRule() {
+ SpiffeJwtSecurityPolicy policy = new SpiffeJwtSecurityPolicy();
+ assertThatThrownBy(() -> policy.beforeWrap(null,
null)).isInstanceOf(IllegalArgumentException.class);
+ policy.setAudience(AUDIENCE);
+ assertThatThrownBy(() -> policy.beforeWrap(null,
null)).isInstanceOf(IllegalStateException.class);
+ policy.setAcceptAnySpiffeId(true);
+ policy.beforeWrap(null, null);
+ policy.setAcceptedSpiffeIds(CALLER);
+ assertThatThrownBy(() -> policy.beforeWrap(null,
null)).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void policyClosesItsOwnedClientAndRecreatesItOnRestart() throws Exception {
+ try (CamelContext context = new DefaultCamelContext();
+ MockedStatic<DefaultWorkloadApiClient> factory =
mockStatic(DefaultWorkloadApiClient.class)) {
+ WorkloadApiClient first = mock(WorkloadApiClient.class);
+ WorkloadApiClient second = mock(WorkloadApiClient.class);
+
factory.when(DefaultWorkloadApiClient::newClient).thenReturn(first, second);
+ SpiffeJwtSecurityPolicy policy = new SpiffeJwtSecurityPolicy();
+ policy.setAudience(AUDIENCE);
+ policy.setAcceptAnySpiffeId(true);
+ Route route = mock(Route.class);
+ when(route.getCamelContext()).thenReturn(context);
+ policy.beforeWrap(route, null);
+ Processor wrapped = policy.wrap(route, e -> {
+ });
+
+ ServiceHelper.startService(wrapped);
+ ServiceHelper.stopService(wrapped);
+ verify(first).close();
+ ServiceHelper.startService(wrapped);
+ ServiceHelper.stopService(wrapped);
+ verify(first).close();
+ verify(second).close();
+ }
+ }
+
+ private static JwtSvid svid(String caller) {
+ JwtSvid svid = mock(JwtSvid.class);
+ when(svid.getSpiffeId()).thenReturn(SpiffeId.parse(caller));
+ when(svid.getExpiry()).thenReturn(new Date(2000000000000L));
+ when(svid.getAudience()).thenReturn(Set.of(AUDIENCE));
+ return svid;
+ }
+
+ private static Exchange exchange(CamelContext context, Object body,
boolean authorization) {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody(body);
+ if (authorization) {
+ exchange.getMessage().setHeader("Authorization", "Bearer " +
TOKEN);
+ }
+ return exchange;
+ }
+
+ private static SpiffeProducer producer(CamelContext context,
WorkloadApiClient client, boolean preserve, String audience) {
+ SpiffeConfiguration configuration = new SpiffeConfiguration();
+ configuration.setOperation(SpiffeOperation.validateJwtSvid);
+ configuration.setAudience(audience);
+ configuration.setPreserveBody(preserve);
+ SpiffeEndpoint endpoint = new SpiffeEndpoint("spiffe:validation", new
SpiffeComponent(context), configuration) {
+ @Override
+ public WorkloadApiClient getWorkloadApiClient() {
+ return client;
+ }
+ };
+ return new SpiffeProducer(endpoint);
+ }
+}
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
index c8e0eb6e61bd..c58c8960287a 100644
---
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
@@ -16,12 +16,20 @@
*/
package org.apache.camel.component.spiffe.integration;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.CamelAuthorizationException;
import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeComponent;
import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.component.spiffe.SpiffeJwtIdentity;
+import org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy;
import org.apache.camel.test.infra.spiffe.services.SpiffeService;
import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
import org.apache.camel.test.junit6.CamelTestSupport;
@@ -55,6 +63,9 @@ class SpiffeWorkloadApiIT extends CamelTestSupport {
return new RouteBuilder() {
@Override
public void configure() {
+ // The JWT policy inherits the component's Workload API
socket; its audience remains policy-owned.
+ getContext().getComponent("spiffe",
SpiffeComponent.class).getConfiguration()
+
.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
from("direct:x509")
.toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
from("direct:jwt")
@@ -64,10 +75,25 @@ class SpiffeWorkloadApiIT extends CamelTestSupport {
from("direct:validateWrongAudience")
.toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
socket());
+ from("direct:jwtPolicy").routeId("jwtPolicy")
+ .process(SpiffeWorkloadApiIT::capturePolicyBody)
+ .policy(jwtPolicy("my-audience"))
+ .setProperty("guardedProcessorRan", constant(true));
+ from("direct:wrongAudiencePolicy")
+ .process(SpiffeWorkloadApiIT::capturePolicyBody)
+ .policy(jwtPolicy("other-audience"))
+ .setProperty("guardedProcessorRan", constant(true));
}
};
}
+ private static SpiffeJwtSecurityPolicy jwtPolicy(String audience) {
+ SpiffeJwtSecurityPolicy policy = new SpiffeJwtSecurityPolicy();
+ policy.setAudience(audience);
+ policy.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return policy;
+ }
+
@Test
void fetchesAnX509SvidForThisWorkload() {
Exchange out = template.request("direct:x509", e -> {
@@ -114,4 +140,62 @@ class SpiffeWorkloadApiIT extends CamelTestSupport {
// the token is valid but minted for my-audience, so validation
against other-audience must fail
assertThat(out.getException()).isInstanceOf(JwtSvidException.class);
}
+
+ @Test
+ void
jwtPolicyPreservesThePostBodyRejectsTheWrongAudienceAndValidatesAgainAfterRouteRestart()
throws Exception {
+ String token = template.requestBody("direct:jwt", null, String.class);
+ byte[] payload = "{\"order\":123}".getBytes(StandardCharsets.UTF_8);
+
+ ByteArrayInputStream body = new ByteArrayInputStream(payload);
+ Exchange allowed = policyRequest("direct:jwtPolicy", token, body);
+ assertThat(allowed.getException()).isNull();
+ assertPolicyBodyPreserved(allowed, payload);
+ assertThat(allowed.getProperty("guardedProcessorRan")).isEqualTo(true);
+ assertThat(allowed.getProperty(SpiffeConstants.JWT_IDENTITY,
SpiffeJwtIdentity.class).getSpiffeId())
+ .isEqualTo(service.getWorkloadSpiffeId());
+
assertThat(allowed.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isNull();
+
assertThat(allowed.getMessage().getHeader(SpiffeConstants.EXPIRY)).isNull();
+
+ ByteArrayInputStream rejectedBody = new ByteArrayInputStream(payload);
+ Exchange rejected = policyRequest("direct:wrongAudiencePolicy", token,
rejectedBody);
+
assertThat(rejected.getException()).isInstanceOf(CamelAuthorizationException.class);
+ assertThat(rejected.getProperty("guardedProcessorRan")).isNull();
+
assertThat(rejected.getProperty(SpiffeConstants.JWT_IDENTITY)).isNull();
+ assertPolicyBodyPreserved(rejected, payload);
+
+ context.getRouteController().stopRoute("jwtPolicy");
+ context.getRouteController().startRoute("jwtPolicy");
+ ByteArrayInputStream restartedBody = new ByteArrayInputStream(payload);
+ Exchange restarted = policyRequest("direct:jwtPolicy", token,
restartedBody);
+ assertThat(restarted.getException()).isNull();
+
assertThat(restarted.getProperty("guardedProcessorRan")).isEqualTo(true);
+ assertPolicyBodyPreserved(restarted, payload);
+ }
+
+ private static void capturePolicyBody(Exchange exchange) throws
IOException {
+ // Camel may cache the caller's stream before entering this route.
Compare at the policy boundary so the
+ // assertion detects policy replacement or consumption, rather than
the framework's stream-cache wrapper.
+ Object body = exchange.getMessage().getBody();
+ exchange.setProperty("bodyBeforeJwtPolicy", body);
+ exchange.setProperty("availableBeforeJwtPolicy", ((InputStream)
body).available());
+ }
+
+ private static void assertPolicyBodyPreserved(Exchange exchange, byte[]
payload) throws IOException {
+ Object actual = exchange.getMessage().getBody();
+
assertThat(actual).isSameAs(exchange.getProperty("bodyBeforeJwtPolicy")).isInstanceOf(InputStream.class);
+ InputStream body = (InputStream) actual;
+
assertThat(body.available()).isEqualTo(exchange.getProperty("availableBeforeJwtPolicy",
Integer.class))
+ .isEqualTo(payload.length);
+ assertThat(body.readAllBytes()).containsExactly(payload);
+ }
+
+ private Exchange policyRequest(String route, String token,
ByteArrayInputStream body) {
+ return template.request(route, exchange -> {
+ exchange.getMessage().setHeader(Exchange.HTTP_METHOD, "POST");
+ exchange.getMessage().setHeader("Authorization", "Bearer " +
token);
+ exchange.getMessage().setHeader(SpiffeConstants.SPIFFE_ID,
"unverified-sender-identity");
+ exchange.setProperty(SpiffeConstants.JWT_IDENTITY,
"stale-identity");
+ exchange.getMessage().setBody(body);
+ });
+ }
}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
index fd413175aaf4..265860dbaefc 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
@@ -676,6 +676,8 @@ public final class ArtifactUtils {
"org.apache.camel.component.shiro.security.ShiroSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-shiro"),
Map.<String, String> entry("SimpleScheduledRoutePolicy",
"org.apache.camel.routepolicy.quartz.SimpleScheduledRoutePolicy|org.apache.camel.spi.RoutePolicy|camel-quartz"),
+ Map.<String, String> entry("SpiffeJwtSecurityPolicy",
+
"org.apache.camel.component.spiffe.SpiffeJwtSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-spiffe"),
Map.<String, String> entry("SpiffeSSLContextParameters",
"org.apache.camel.component.spiffe.SpiffeSSLContextParameters|org.apache.camel.support.jsse.SSLContextParameters|camel-spiffe"),
Map.<String, String> entry("SpiffeSecurityPolicy",
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
index 1ecc13436cde..0b5c2ea51b8e 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
@@ -132,6 +132,27 @@ public interface SpiffeComponentBuilderFactory {
}
+ /**
+ * Whether validateJwtSvid leaves the application body untouched and
+ * publishes the verified identity through the CamelSpiffeJwtIdentity
+ * exchange property. In this mode the token must be in
CamelSpiffeToken
+ * or an Authorization Bearer header: the body is never read as a
token.
+ * The default returns the JwtSvid in the body as before.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: producer
+ *
+ * @param preserveBody the value to set
+ * @return the dsl builder
+ */
+ default SpiffeComponentBuilder preserveBody(boolean preserveBody) {
+ doSetProperty("preserveBody", preserveBody);
+ return this;
+ }
+
+
/**
* Whether autowiring is enabled. This is used for automatic autowiring
* options (the option must be marked as autowired) by looking up in
the
@@ -257,6 +278,7 @@ public interface SpiffeComponentBuilderFactory {
case "configuration": ((SpiffeComponent)
component).setConfiguration((org.apache.camel.component.spiffe.SpiffeConfiguration)
value); return true;
case "lazyStartProducer": ((SpiffeComponent)
component).setLazyStartProducer((boolean) value); return true;
case "operation": getOrCreateConfiguration((SpiffeComponent)
component).setOperation((org.apache.camel.component.spiffe.SpiffeOperation)
value); return true;
+ case "preserveBody": getOrCreateConfiguration((SpiffeComponent)
component).setPreserveBody((boolean) value); return true;
case "autowiredEnabled": ((SpiffeComponent)
component).setAutowiredEnabled((boolean) value); return true;
case "workloadApiClient":
getOrCreateConfiguration((SpiffeComponent)
component).setWorkloadApiClient((io.spiffe.workloadapi.WorkloadApiClient)
value); return true;
case "allowOperationHeader":
getOrCreateConfiguration((SpiffeComponent)
component).setAllowOperationHeader((boolean) value); return true;
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
index 3d9bcef65c5b..a2bdcbc73f93 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
@@ -96,6 +96,44 @@ public interface SpiffeEndpointBuilderFactory {
doSetProperty("operation", operation);
return this;
}
+ /**
+ * Whether validateJwtSvid leaves the application body untouched and
+ * publishes the verified identity through the CamelSpiffeJwtIdentity
+ * exchange property. In this mode the token must be in
CamelSpiffeToken
+ * or an Authorization Bearer header: the body is never read as a
token.
+ * The default returns the JwtSvid in the body as before.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: producer
+ *
+ * @param preserveBody the value to set
+ * @return the dsl builder
+ */
+ default SpiffeEndpointBuilder preserveBody(boolean preserveBody) {
+ doSetProperty("preserveBody", preserveBody);
+ return this;
+ }
+ /**
+ * Whether validateJwtSvid leaves the application body untouched and
+ * publishes the verified identity through the CamelSpiffeJwtIdentity
+ * exchange property. In this mode the token must be in
CamelSpiffeToken
+ * or an Authorization Bearer header: the body is never read as a
token.
+ * The default returns the JwtSvid in the body as before.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: producer
+ *
+ * @param preserveBody the value to set
+ * @return the dsl builder
+ */
+ default SpiffeEndpointBuilder preserveBody(String preserveBody) {
+ doSetProperty("preserveBody", preserveBody);
+ return this;
+ }
/**
* Whether the CamelSpiffeOperation header may override the configured
* operation. Disabled by default: the operation decides whether this
@@ -412,8 +450,9 @@ public interface SpiffeEndpointBuilderFactory {
return "CamelSpiffeSpiffeId";
}
/**
- * The expiry of the returned SVID: the token expiry for fetchJwtSvid,
- * or the leaf certificate's notAfter for fetchX509Svid.
+ * The expiry of the returned SVID: the token expiry for fetchJwtSvid
or
+ * validateJwtSvid, or the leaf certificate's notAfter for
+ * fetchX509Svid.
*
* The option is a: {@code java.util.Date} type.
*