This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 895b7647b9b7 [backport camel-4.18.x] CAMEL-24319: camel-keycloak - add
optional token type (typ) and authorized party (azp) validation (#25259)
(#25307)
895b7647b9b7 is described below
commit 895b7647b9b7516adda0b6c0095284c13f6a6554
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Aug 3 13:32:24 2026 +0200
[backport camel-4.18.x] CAMEL-24319: camel-keycloak - add optional token
type (typ) and authorized party (azp) validation (#25259) (#25307)
Opt-in expectedTokenTypes + expectedAuthorizedParty on
KeycloakSecurityPolicy,
applied on all three token-validation paths (authenticateToken,
validateRoles,
validatePermissions) for both local JWT verification and token
introspection.
Disabled by default (non-breaking). The documentation page is main-only.
(cherry picked from commit bb4922598c39fef67b39f3a56b6b17b1a0e5b0c8)
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../keycloak/security/KeycloakSecurityHelper.java | 55 ++++++++++++
.../keycloak/security/KeycloakSecurityPolicy.java | 68 +++++++++++++++
.../security/KeycloakSecurityProcessor.java | 81 +++++++++++++++++-
.../security/KeycloakSecurityHelperTest.java | 97 ++++++++++++++++++++++
.../security/KeycloakSecurityProcessorTest.java | 79 ++++++++++++++++++
5 files changed, 379 insertions(+), 1 deletion(-)
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
index 39549c1c6034..a274f9ce9406 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelper.java
@@ -89,6 +89,36 @@ public final class KeycloakSecurityHelper {
public static AccessToken parseAndVerifyAccessToken(
String tokenString, PublicKey publicKey, String expectedIssuer,
List<String> expectedAudiences)
throws VerificationException {
+ return parseAndVerifyAccessToken(tokenString, publicKey,
expectedIssuer, expectedAudiences, null, null);
+ }
+
+ /**
+ * Parses and fully verifies an access token including signature, issuer
and, optionally, audience, token type
+ * ({@code typ}) and authorized party ({@code azp}) validation. This is
the recommended method for secure token
+ * validation.
+ *
+ * @param tokenString the JWT token string
+ * @param publicKey the public key for signature
verification
+ * @param expectedIssuer the expected issuer URL (e.g.,
"http://localhost:8080/realms/myrealm")
+ * @param expectedAudiences the expected audiences; when non-empty,
the token's "aud" claim must contain
+ * every one of them (matching Keycloak's
own
+ * {@link
TokenVerifier#audience(String...)} check). Pass null or an empty list to
+ * skip audience validation.
+ * @param expectedTokenTypes the accepted token types; when
non-empty, the token's {@code typ} claim must be
+ * one of them. Guards against token-type
confusion (e.g. an ID or refresh token
+ * presented where an access token is
expected). Pass null or an empty list to skip
+ * token-type validation.
+ * @param expectedAuthorizedParty the expected authorized party; when
non-empty, the token's {@code azp} claim must
+ * equal it. Ensures the token was issued
for the expected client. Pass null or an
+ * empty string to skip authorized-party
validation.
+ * @return the verified access token
+ * @throws VerificationException if verification fails (invalid
signature, wrong issuer, expired, missing/wrong
+ * audience, wrong token type, wrong
authorized party, etc.)
+ */
+ public static AccessToken parseAndVerifyAccessToken(
+ String tokenString, PublicKey publicKey, String expectedIssuer,
List<String> expectedAudiences,
+ List<String> expectedTokenTypes, String expectedAuthorizedParty)
+ throws VerificationException {
if (publicKey == null) {
throw new VerificationException("Public key is required for secure
token verification");
}
@@ -119,6 +149,31 @@ public final class KeycloakSecurityHelper {
String.format("Token issuer mismatch: expected '%s' but
got '%s'", expectedIssuer, actualIssuer));
}
+ // Optional token type (typ) allow-list — guards against token-type
confusion, e.g. an ID or refresh token
+ // being presented where an access token is expected.
+ if (expectedTokenTypes != null && !expectedTokenTypes.isEmpty()) {
+ String actualType = token.getType();
+ if (actualType == null ||
!expectedTokenTypes.contains(actualType)) {
+ LOG.error("SECURITY: Token type mismatch - expected one of {}
but got '{}'",
+ expectedTokenTypes, actualType);
+ throw new VerificationException(
+ String.format("Token type mismatch: expected one of %s
but got '%s'",
+ expectedTokenTypes, actualType));
+ }
+ }
+
+ // Optional authorized party (azp) check — ensures the token was
issued for the expected client.
+ if (expectedAuthorizedParty != null &&
!expectedAuthorizedParty.isEmpty()) {
+ String actualAzp = token.getIssuedFor();
+ if (!expectedAuthorizedParty.equals(actualAzp)) {
+ LOG.error("SECURITY: Token authorized party (azp) mismatch -
expected '{}' but got '{}'",
+ expectedAuthorizedParty, actualAzp);
+ throw new VerificationException(
+ String.format("Token authorized party mismatch:
expected '%s' but got '%s'",
+ expectedAuthorizedParty, actualAzp));
+ }
+ }
+
LOG.debug("Token successfully verified for issuer: {}",
expectedIssuer);
return token;
}
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
index 17781c45ec21..55d76ea17b9c 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityPolicy.java
@@ -106,6 +106,21 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
*/
private String expectedAudience;
+ /**
+ * Comma-separated list of accepted token types ({@code typ} claim). When
set, a token whose {@code typ} is not one
+ * of the configured values is rejected. This guards against token-type
confusion, e.g. an ID token or refresh token
+ * being presented where an access token is expected. Disabled by default
for backward compatibility. Example:
+ * "Bearer"
+ */
+ private String expectedTokenTypes;
+
+ /**
+ * Expected authorized party ({@code azp} claim). When set, a token whose
{@code azp} does not equal the configured
+ * value is rejected, ensuring the token was issued for the expected
client. Disabled by default for backward
+ * compatibility. Example: "my-client"
+ */
+ private String expectedAuthorizedParty;
+
public KeycloakSecurityPolicy() {
this.requiredRoles = "";
this.requiredPermissions = "";
@@ -470,4 +485,57 @@ public class KeycloakSecurityPolicy implements
AuthorizationPolicy {
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
+
+ /**
+ * Gets the accepted token type(s) as a comma-separated string.
+ *
+ * @return comma-separated token types (e.g., "Bearer"), or null if not
configured
+ */
+ public String getExpectedTokenTypes() {
+ return expectedTokenTypes;
+ }
+
+ /**
+ * Sets the accepted token type(s) as a comma-separated string. When set,
a token whose "typ" claim is not one of
+ * the configured values is rejected.
+ *
+ * @param expectedTokenTypes comma-separated token types (e.g., "Bearer")
+ */
+ public void setExpectedTokenTypes(String expectedTokenTypes) {
+ this.expectedTokenTypes = expectedTokenTypes;
+ }
+
+ /**
+ * Gets the accepted token types as a list.
+ *
+ * @return list of accepted token types, or an empty list if not configured
+ */
+ public List<String> getExpectedTokenTypesAsList() {
+ if (ObjectHelper.isEmpty(expectedTokenTypes)) {
+ return Collections.emptyList();
+ }
+ return Arrays.stream(expectedTokenTypes.split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Gets the expected authorized party ({@code azp}).
+ *
+ * @return the expected authorized party, or null if not configured
+ */
+ public String getExpectedAuthorizedParty() {
+ return expectedAuthorizedParty;
+ }
+
+ /**
+ * Sets the expected authorized party ({@code azp}). When set, a token
whose "azp" claim does not equal the
+ * configured value is rejected.
+ *
+ * @param expectedAuthorizedParty the expected authorized party (e.g.,
"my-client")
+ */
+ public void setExpectedAuthorizedParty(String expectedAuthorizedParty) {
+ this.expectedAuthorizedParty = expectedAuthorizedParty;
+ }
}
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
index df9f958c4871..6ccf59c39ddd 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessor.java
@@ -114,6 +114,14 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
if (!policy.getExpectedAudienceAsList().isEmpty()) {
validateAudienceFromIntrospection(introspectionResult,
exchange);
}
+
+ if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+ validateTokenTypeFromIntrospection(introspectionResult,
exchange);
+ }
+
+ if (!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+ validateAuthorizedPartyFromIntrospection(introspectionResult,
exchange);
+ }
} else {
parseAndVerifyToken(accessToken, exchange);
}
@@ -283,6 +291,15 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
validateAudienceFromIntrospection(introspectionResult,
exchange);
}
+ // Validate token type / authorized party from introspection
result if configured
+ if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+ validateTokenTypeFromIntrospection(introspectionResult,
exchange);
+ }
+
+ if
(!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+
validateAuthorizedPartyFromIntrospection(introspectionResult, exchange);
+ }
+
userRoles =
KeycloakSecurityHelper.extractRolesFromIntrospection(
introspectionResult, policy.getRealm(),
policy.getClientId());
} else {
@@ -339,7 +356,8 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
if (publicKey != null) {
try {
return KeycloakSecurityHelper.parseAndVerifyAccessToken(
- accessToken, publicKey, expectedIssuer,
policy.getExpectedAudienceAsList());
+ accessToken, publicKey, expectedIssuer,
policy.getExpectedAudienceAsList(),
+ policy.getExpectedTokenTypesAsList(),
policy.getExpectedAuthorizedParty());
} catch (VerificationException e) {
LOG.error("Token verification failed: {}", e.getMessage());
throw new CamelAuthorizationException("Token verification
failed: " + e.getMessage(), exchange, e);
@@ -415,6 +433,57 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
LOG.debug("Audience validation from introspection successful: {}",
expectedAudiences);
}
+ /**
+ * Validates the token type ({@code typ}) from an introspection result.
When token-type validation is configured, a
+ * token whose {@code typ} is missing or not among the accepted values is
rejected — guarding against token-type
+ * confusion (e.g. an ID or refresh token presented where an access token
is expected).
+ */
+ private void validateTokenTypeFromIntrospection(
+ KeycloakTokenIntrospector.IntrospectionResult introspectionResult,
Exchange exchange)
+ throws CamelAuthorizationException {
+ List<String> expectedTokenTypes = policy.getExpectedTokenTypesAsList();
+ // Use the JWT "typ" claim (token category, e.g. Bearer/Refresh/ID),
which Keycloak forwards on its
+ // introspection response — not the RFC 7662 "token_type" field, which
is the OAuth token type ("Bearer")
+ // and does not distinguish access from refresh/ID tokens, i.e. it
cannot express what this check validates.
+ Object typeClaim = introspectionResult.getClaim("typ");
+ String actualType = typeClaim instanceof String s ? s : null;
+
+ if (actualType == null || !expectedTokenTypes.contains(actualType)) {
+ LOG.error("SECURITY: Token type mismatch from introspection -
expected one of {} but got '{}'",
+ expectedTokenTypes, actualType);
+ throw new CamelAuthorizationException(
+ String.format("Token type mismatch: expected one of %s but
got '%s'",
+ expectedTokenTypes, actualType),
+ exchange);
+ }
+
+ LOG.debug("Token type validation from introspection successful: {}",
expectedTokenTypes);
+ }
+
+ /**
+ * Validates the authorized party ({@code azp}) from an introspection
result. When authorized-party validation is
+ * configured, a token whose {@code azp} does not equal the expected value
is rejected — ensuring the token was
+ * issued for the expected client.
+ */
+ private void validateAuthorizedPartyFromIntrospection(
+ KeycloakTokenIntrospector.IntrospectionResult introspectionResult,
Exchange exchange)
+ throws CamelAuthorizationException {
+ String expectedAuthorizedParty = policy.getExpectedAuthorizedParty();
+ Object azpClaim = introspectionResult.getClaim("azp");
+ String actualAzp = azpClaim instanceof String s ? s : null;
+
+ if (!expectedAuthorizedParty.equals(actualAzp)) {
+ LOG.error("SECURITY: Token authorized party (azp) mismatch from
introspection - expected '{}' but got '{}'",
+ expectedAuthorizedParty, actualAzp);
+ throw new CamelAuthorizationException(
+ String.format("Token authorized party mismatch: expected
'%s' but got '%s'",
+ expectedAuthorizedParty, actualAzp),
+ exchange);
+ }
+
+ LOG.debug("Authorized party validation from introspection successful:
{}", expectedAuthorizedParty);
+ }
+
private void validatePermissions(String accessToken, Exchange exchange)
throws Exception {
try {
Set<String> userPermissions;
@@ -439,6 +508,16 @@ public class KeycloakSecurityProcessor extends
DelegateProcessor {
validateAudienceFromIntrospection(introspectionResult,
exchange);
}
+ // Validate token type and authorized party too, otherwise
these checks could be bypassed by
+ // configuring only permissions (which skips
authenticateToken/validateRoles) with introspection.
+ if (!policy.getExpectedTokenTypesAsList().isEmpty()) {
+ validateTokenTypeFromIntrospection(introspectionResult,
exchange);
+ }
+
+ if
(!ObjectHelper.isEmpty(policy.getExpectedAuthorizedParty())) {
+
validateAuthorizedPartyFromIntrospection(introspectionResult, exchange);
+ }
+
userPermissions =
KeycloakSecurityHelper.extractPermissionsFromIntrospection(introspectionResult);
} else {
// Use local JWT parsing with secure verification
diff --git
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
index 90639e8374d1..539417472e9b 100644
---
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
+++
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityHelperTest.java
@@ -224,6 +224,103 @@ public class KeycloakSecurityHelperTest {
assertEquals(expectedIssuer, verified.getIssuer());
}
+ @Test
+ void testParseAndVerifyAccessTokenAcceptsMatchingTokenType() throws
Exception {
+ String expectedIssuer = "http://localhost:8080/realms/test";
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ AccessToken token = new AccessToken();
+ token.issuer(expectedIssuer);
+ token.subject("user-123");
+ token.exp(System.currentTimeMillis() / 1000 + 3600);
+ token.type("Bearer");
+
+ String signed = new JWSBuilder()
+ .type("JWT")
+ .jsonContent(token)
+ .rsa256(keyPair.getPrivate());
+
+ AccessToken verified =
KeycloakSecurityHelper.parseAndVerifyAccessToken(
+ signed, keyPair.getPublic(), expectedIssuer, null,
List.of("Bearer"), null);
+ assertEquals("user-123", verified.getSubject());
+ }
+
+ @Test
+ void testParseAndVerifyAccessTokenRejectsWrongTokenType() throws Exception
{
+ String expectedIssuer = "http://localhost:8080/realms/test";
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ // An ID token presented where an access token (typ=Bearer) is
expected.
+ AccessToken token = new AccessToken();
+ token.issuer(expectedIssuer);
+ token.subject("user-123");
+ token.exp(System.currentTimeMillis() / 1000 + 3600);
+ token.type("ID");
+
+ String signed = new JWSBuilder()
+ .type("JWT")
+ .jsonContent(token)
+ .rsa256(keyPair.getPrivate());
+
+ assertThrows(VerificationException.class,
+ () -> KeycloakSecurityHelper.parseAndVerifyAccessToken(
+ signed, keyPair.getPublic(), expectedIssuer, null,
List.of("Bearer"), null));
+ }
+
+ @Test
+ void testParseAndVerifyAccessTokenAcceptsMatchingAuthorizedParty() throws
Exception {
+ String expectedIssuer = "http://localhost:8080/realms/test";
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ AccessToken token = new AccessToken();
+ token.issuer(expectedIssuer);
+ token.subject("user-123");
+ token.exp(System.currentTimeMillis() / 1000 + 3600);
+ token.issuedFor("my-client");
+
+ String signed = new JWSBuilder()
+ .type("JWT")
+ .jsonContent(token)
+ .rsa256(keyPair.getPrivate());
+
+ AccessToken verified =
KeycloakSecurityHelper.parseAndVerifyAccessToken(
+ signed, keyPair.getPublic(), expectedIssuer, null, null,
"my-client");
+ assertEquals("user-123", verified.getSubject());
+ }
+
+ @Test
+ void testParseAndVerifyAccessTokenRejectsWrongAuthorizedParty() throws
Exception {
+ String expectedIssuer = "http://localhost:8080/realms/test";
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ AccessToken token = new AccessToken();
+ token.issuer(expectedIssuer);
+ token.subject("user-123");
+ token.exp(System.currentTimeMillis() / 1000 + 3600);
+ token.issuedFor("other-client");
+
+ String signed = new JWSBuilder()
+ .type("JWT")
+ .jsonContent(token)
+ .rsa256(keyPair.getPrivate());
+
+ assertThrows(VerificationException.class,
+ () -> KeycloakSecurityHelper.parseAndVerifyAccessToken(
+ signed, keyPair.getPublic(), expectedIssuer, null,
null, "my-client"));
+ }
+
@Test
void testExtractKeyIdReturnsKidFromHeader() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
diff --git
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
index c3c1c2f8f269..c13a39723e0b 100644
---
a/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
+++
b/components/camel-keycloak/src/test/java/org/apache/camel/component/keycloak/security/KeycloakSecurityProcessorTest.java
@@ -372,4 +372,83 @@ class KeycloakSecurityProcessorTest {
assertFalse(routeReached.get(),
"Route body must not be reached when the token has the
required permission but not the expected audience");
}
+
+ @Test
+ void testTokenWrongTokenTypeRejectedWithRequiredPermissionsIntrospection()
throws Exception {
+ // Token satisfies the required permission but carries the wrong token
type: validatePermissions() must
+ // still run the token-type check on the introspection path, otherwise
it could be bypassed with a
+ // permissions-only configuration.
+ KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+ "http://localhost:8080", "test-realm", "test-client",
"test-secret", (TokenCache) null) {
+ @Override
+ public IntrospectionResult introspect(String token) {
+ return new IntrospectionResult(Map.of("active", true, "scope",
"read", "typ", "Refresh"));
+ }
+ };
+
+ KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy() {
+ @Override
+ public boolean isUseTokenIntrospection() {
+ return true;
+ }
+
+ @Override
+ public KeycloakTokenIntrospector getTokenIntrospector() {
+ return introspector;
+ }
+ };
+ policy.setServerUrl("http://localhost:8080");
+ policy.setRealm("test-realm");
+ policy.setClientId("test-client");
+ policy.setClientSecret("test-secret");
+ policy.setValidateIssuer(false);
+ policy.setExpectedTokenTypes("Bearer");
+ policy.setRequiredPermissions("read");
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertFalse(routeReached.get(),
+ "Route body must not be reached when the token has the
required permission but the wrong token type");
+ }
+
+ @Test
+ void
testTokenWrongAuthorizedPartyRejectedWithRequiredPermissionsIntrospection()
throws Exception {
+ // Token satisfies the required permission but carries the wrong
authorized party (azp): validatePermissions()
+ // must still run the azp check on the introspection path.
+ KeycloakTokenIntrospector introspector = new KeycloakTokenIntrospector(
+ "http://localhost:8080", "test-realm", "test-client",
"test-secret", (TokenCache) null) {
+ @Override
+ public IntrospectionResult introspect(String token) {
+ return new IntrospectionResult(Map.of("active", true, "scope",
"read", "azp", "attacker-client"));
+ }
+ };
+
+ KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy() {
+ @Override
+ public boolean isUseTokenIntrospection() {
+ return true;
+ }
+
+ @Override
+ public KeycloakTokenIntrospector getTokenIntrospector() {
+ return introspector;
+ }
+ };
+ policy.setServerUrl("http://localhost:8080");
+ policy.setRealm("test-realm");
+ policy.setClientId("test-client");
+ policy.setClientSecret("test-secret");
+ policy.setValidateIssuer(false);
+ policy.setExpectedAuthorizedParty("expected-client");
+ policy.setRequiredPermissions("read");
+
+ AtomicBoolean routeReached = new AtomicBoolean(false);
+ KeycloakSecurityProcessor processor = new KeycloakSecurityProcessor(e
-> routeReached.set(true), policy);
+
+ assertThrows(CamelAuthorizationException.class, () ->
processor.process(bearer("x")));
+ assertFalse(routeReached.get(),
+ "Route body must not be reached when the token has the
required permission but the wrong authorized party");
+ }
}