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 8df5541daafd CAMEL-24833: camel-spiffe - validateJwtSvid falls back to
the Authorization: Bearer header (#27077)
8df5541daafd is described below
commit 8df5541daafd137749da8322ebdbec594369a58a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 30 12:56:43 2026 +0200
CAMEL-24833: camel-spiffe - validateJwtSvid falls back to the
Authorization: Bearer header (#27077)
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../camel/catalog/docs/spiffe-component.adoc | 32 +++++++--
.../src/main/docs/spiffe-component.adoc | 32 +++++++--
.../camel/component/spiffe/SpiffeProducer.java | 27 +++++++-
.../camel/component/spiffe/SpiffeProducerTest.java | 78 ++++++++++++++++++++++
4 files changed, 160 insertions(+), 9 deletions(-)
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 01c35bece736..c9c1930d1ca5 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
@@ -62,10 +62,14 @@ The component supports the following producer operations:
header to its SPIFFE ID.
* `fetchJwtSvid` — fetches a JWT-SVID for the configured `audience` (or the
`CamelSpiffeAudience` header). The
message body is set to the JWT token string, with the `CamelSpiffeSpiffeId`
and `CamelSpiffeExpiry` headers.
-* `validateJwtSvid` — validates the JWT-SVID passed in the `CamelSpiffeToken`
header (or the body) against the
-`audience`. When several audiences are configured the token is accepted if it
matches *any* of them — the
-Workload API validates one audience at a time, so each is tried in turn. The
message body is set to the validated
-`io.spiffe.svid.jwtsvid.JwtSvid`.
+* `validateJwtSvid` — validates a JWT-SVID against the `audience`. The token
is taken from the first of:
+the `CamelSpiffeToken` header, an `Authorization: Bearer <token>` header (the
scheme matched case-insensitively), or
+the message body. The `Authorization` header is tried *before* the body so a
request payload on a `POST`/`PUT` is not
+mistaken for the token; this lets a `platform-http` route validate an incoming
bearer token without a bean to strip
+the scheme. On success the message body is set to the validated
`io.spiffe.svid.jwtsvid.JwtSvid`, so a route that
+needs the original request payload afterwards must keep a copy before
validating (or validate a bodiless request).
+When several audiences are configured the token is accepted if it matches
*any* of them — the Workload API validates
+one audience at a time, so each is tried in turn.
[NOTE]
====
@@ -88,6 +92,26 @@ from("direct:start")
.to("http://backend.example.org/api");
------------------------------------------------------------
+Validate an incoming bearer token on a `platform-http` route, without a bean
to strip the scheme:
+
+[source,yaml]
+------------------------------------------------------------
+- from:
+ uri: "platform-http:/api"
+ steps:
+ # the Authorization: Bearer <token> header is picked up automatically;
validation replaces the body with the
+ # JwtSvid, so this fits a request whose payload is not needed afterwards
+ - to:
"spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/api"
+ # the token is a credential; drop it before the exchange goes further
+ - removeHeaders:
+ pattern: "Authorization"
+ - to: "direct:handleRequest"
+------------------------------------------------------------
+
+A missing token fails with `IllegalArgumentException`; a rejected one
(invalid, expired, or a wrong audience) fails
+with `io.spiffe.exception.JwtSvidException`. Catch both —
`onException(io.spiffe.exception.JwtSvidException.class,
+IllegalArgumentException.class)` — to answer `401`.
+
== Mutual TLS with SPIFFE (SSLContextParameters)
For X.509-based zero-trust mTLS, the component provides
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index 01c35bece736..c9c1930d1ca5 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -62,10 +62,14 @@ The component supports the following producer operations:
header to its SPIFFE ID.
* `fetchJwtSvid` — fetches a JWT-SVID for the configured `audience` (or the
`CamelSpiffeAudience` header). The
message body is set to the JWT token string, with the `CamelSpiffeSpiffeId`
and `CamelSpiffeExpiry` headers.
-* `validateJwtSvid` — validates the JWT-SVID passed in the `CamelSpiffeToken`
header (or the body) against the
-`audience`. When several audiences are configured the token is accepted if it
matches *any* of them — the
-Workload API validates one audience at a time, so each is tried in turn. The
message body is set to the validated
-`io.spiffe.svid.jwtsvid.JwtSvid`.
+* `validateJwtSvid` — validates a JWT-SVID against the `audience`. The token
is taken from the first of:
+the `CamelSpiffeToken` header, an `Authorization: Bearer <token>` header (the
scheme matched case-insensitively), or
+the message body. The `Authorization` header is tried *before* the body so a
request payload on a `POST`/`PUT` is not
+mistaken for the token; this lets a `platform-http` route validate an incoming
bearer token without a bean to strip
+the scheme. On success the message body is set to the validated
`io.spiffe.svid.jwtsvid.JwtSvid`, so a route that
+needs the original request payload afterwards must keep a copy before
validating (or validate a bodiless request).
+When several audiences are configured the token is accepted if it matches
*any* of them — the Workload API validates
+one audience at a time, so each is tried in turn.
[NOTE]
====
@@ -88,6 +92,26 @@ from("direct:start")
.to("http://backend.example.org/api");
------------------------------------------------------------
+Validate an incoming bearer token on a `platform-http` route, without a bean
to strip the scheme:
+
+[source,yaml]
+------------------------------------------------------------
+- from:
+ uri: "platform-http:/api"
+ steps:
+ # the Authorization: Bearer <token> header is picked up automatically;
validation replaces the body with the
+ # JwtSvid, so this fits a request whose payload is not needed afterwards
+ - to:
"spiffe:auth?operation=validateJwtSvid&audience=spiffe://example.org/api"
+ # the token is a credential; drop it before the exchange goes further
+ - removeHeaders:
+ pattern: "Authorization"
+ - to: "direct:handleRequest"
+------------------------------------------------------------
+
+A missing token fails with `IllegalArgumentException`; a rejected one
(invalid, expired, or a wrong audience) fails
+with `io.spiffe.exception.JwtSvidException`. Catch both —
`onException(io.spiffe.exception.JwtSvidException.class,
+IllegalArgumentException.class)` — to answer `401`.
+
== 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/SpiffeProducer.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
index 227e1bddb0fa..e068eceac645 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
@@ -69,12 +69,19 @@ 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));
+ }
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 or the body)");
+ "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
@@ -111,6 +118,24 @@ public class SpiffeProducer extends DefaultProducer {
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) {
SpiffeOperation configured =
getEndpoint().getConfiguration().getOperation();
if (!getEndpoint().getConfiguration().isAllowOperationHeader()) {
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
index 2f05a90163a4..d0367b598e75 100644
---
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
@@ -146,4 +146,82 @@ class SpiffeProducerTest extends CamelTestSupport {
assertThat(out.getMessage().getBody()).isSameAs(svid);
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/client");
}
+
+ @Test
+ void validateJwtSvidTokenFromAuthorizationHeader() throws Exception {
+ SpiffeId id = spiffeId("spiffe://example.org/client");
+ JwtSvid svid = mock(JwtSvid.class);
+ when(svid.getSpiffeId()).thenReturn(id);
+ when(client.validateJwtSvid("auth-token",
"my-audience")).thenReturn(svid);
+
+ // no CamelSpiffeToken header and no body -> the producer falls back
to the Authorization: Bearer header
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=validateJwtSvid&audience=my-audience",
+ e -> e.getIn().setHeader("Authorization", "Bearer
auth-token"));
+
+ assertThat(out.getMessage().getBody()).isSameAs(svid);
+
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/client");
+ }
+
+ @Test
+ void validateJwtSvidBearerSchemeIsCaseInsensitive() throws Exception {
+ SpiffeId id = spiffeId("spiffe://example.org/client");
+ JwtSvid svid = mock(JwtSvid.class);
+ when(svid.getSpiffeId()).thenReturn(id);
+ when(client.validateJwtSvid("ci-token",
"my-audience")).thenReturn(svid);
+
+ // scheme matched case-insensitively and the token trimmed
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=validateJwtSvid&audience=my-audience",
+ e -> e.getIn().setHeader("Authorization", "bearer
ci-token"));
+
+ assertThat(out.getMessage().getBody()).isSameAs(svid);
+ }
+
+ @Test
+ void validateJwtSvidHeaderTokenWinsOverAuthorization() throws Exception {
+ SpiffeId id = spiffeId("spiffe://example.org/client");
+ JwtSvid svid = mock(JwtSvid.class);
+ when(svid.getSpiffeId()).thenReturn(id);
+ // only the CamelSpiffeToken value is stubbed; if the Authorization
value were used the mock returns null
+ when(client.validateJwtSvid("explicit-token",
"my-audience")).thenReturn(svid);
+
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=validateJwtSvid&audience=my-audience",
+ e -> {
+ e.getIn().setHeader(SpiffeConstants.TOKEN,
"explicit-token");
+ e.getIn().setHeader("Authorization", "Bearer
ignored-token");
+ });
+
+ assertThat(out.getMessage().getBody()).isSameAs(svid);
+ }
+
+ @Test
+ void validateJwtSvidNonBearerAuthorizationFails() {
+ // a non-bearer Authorization value is not a token source; the request
falls through to the token-required error
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=validateJwtSvid&audience=my-audience",
+ e -> e.getIn().setHeader("Authorization", "Basic
dXNlcjpwYXNz"));
+
+ assertThat(out.isFailed()).isTrue();
+
assertThat(out.getException()).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void validateJwtSvidAuthorizationWinsOverBody() throws Exception {
+ SpiffeId id = spiffeId("spiffe://example.org/client");
+ JwtSvid svid = mock(JwtSvid.class);
+ when(svid.getSpiffeId()).thenReturn(id);
+ // only the Authorization token is stubbed: a POST/PUT request payload
in the body must not be taken as the token
+ when(client.validateJwtSvid("bearer-token",
"my-audience")).thenReturn(svid);
+
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=validateJwtSvid&audience=my-audience",
+ e -> {
+ e.getIn().setBody("the-request-payload");
+ e.getIn().setHeader("Authorization", "Bearer
bearer-token");
+ });
+
+ assertThat(out.getMessage().getBody()).isSameAs(svid);
+ }
}