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);
+    }
 }

Reply via email to