diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc index d18ac18c1c567..1700046c008df 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc @@ -116,6 +116,34 @@ beans: `expectedAudience` accepts a comma-separated list (e.g., `"my-client,my-other-client"`) when a token must carry several audiences at once. Disabled by default for backward compatibility. +=== Token Type and Authorized Party Validation + +By default, the security policy does not check the token's `typ` (token type) or `azp` (authorized party) claim. +Two optional settings tighten this: + +* `expectedTokenTypes` - a comma-separated allow-list of accepted `typ` values (e.g., `"Bearer"`). When set, a +token whose `typ` is not one of the configured values is rejected. This guards against token-type confusion, +for example an ID token or refresh token being presented where an access token is expected. +* `expectedAuthorizedParty` - the expected `azp` value (e.g., `"my-client"`). When set, a token whose `azp` does +not equal the configured value is rejected, ensuring the token was issued for the expected client. + +Both checks apply to local JWT verification and to token introspection, and both are disabled by default for +backward compatibility. + +[source,java] +---- +KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy( + "http://localhost:8080", "my-realm", "my-client", "client-secret"); + +// Only accept access tokens (typ=Bearer) issued for "my-client" +policy.setExpectedTokenTypes("Bearer"); +policy.setExpectedAuthorizedParty("my-client"); + +from("direct:protected") + .policy(policy) + .to("mock:result"); +---- + === Role-based Authorization [tabs] diff --git a/components/camel-keycloak/src/main/docs/keycloak-security.adoc b/components/camel-keycloak/src/main/docs/keycloak-security.adoc index d18ac18c1c567..1700046c008df 100644 --- a/components/camel-keycloak/src/main/docs/keycloak-security.adoc +++ b/components/camel-keycloak/src/main/docs/keycloak-security.adoc @@ -116,6 +116,34 @@ beans: `expectedAudience` accepts a comma-separated list (e.g., `"my-client,my-other-client"`) when a token must carry several audiences at once. Disabled by default for backward compatibility. +=== Token Type and Authorized Party Validation + +By default, the security policy does not check the token's `typ` (token type) or `azp` (authorized party) claim. +Two optional settings tighten this: + +* `expectedTokenTypes` - a comma-separated allow-list of accepted `typ` values (e.g., `"Bearer"`). When set, a +token whose `typ` is not one of the configured values is rejected. This guards against token-type confusion, +for example an ID token or refresh token being presented where an access token is expected. +* `expectedAuthorizedParty` - the expected `azp` value (e.g., `"my-client"`). When set, a token whose `azp` does +not equal the configured value is rejected, ensuring the token was issued for the expected client. + +Both checks apply to local JWT verification and to token introspection, and both are disabled by default for +backward compatibility. + +[source,java] +---- +KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy( + "http://localhost:8080", "my-realm", "my-client", "client-secret"); + +// Only accept access tokens (typ=Bearer) issued for "my-client" +policy.setExpectedTokenTypes("Bearer"); +policy.setExpectedAuthorizedParty("my-client"); + +from("direct:protected") + .policy(policy) + .to("mock:result"); +---- + === Role-based Authorization [tabs] 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 f529de8050745..02277592b4775 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 @@ -90,6 +90,36 @@ public static AccessToken parseAndVerifyAccessToken(String tokenString, PublicKe public static AccessToken parseAndVerifyAccessToken( String tokenString, PublicKey publicKey, String expectedIssuer, List 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 expectedAudiences, + List expectedTokenTypes, String expectedAuthorizedParty) + throws VerificationException { if (publicKey == null) { throw new VerificationException("Public key is required for secure token verification"); } @@ -120,6 +150,31 @@ public static AccessToken parseAndVerifyAccessToken( 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 17781c45ec21c..55d76ea17b9c7 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 List getExpectedAudienceAsList() { .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 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 943f3127079b6..fb2fd7d34f3e8 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 @@ -115,6 +115,14 @@ private void authenticateToken(String accessToken, Exchange exchange) throws Exc 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); } @@ -284,6 +292,15 @@ private void validateRoles(String accessToken, Exchange exchange) throws Excepti 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 { @@ -340,7 +357,8 @@ private AccessToken parseAndVerifyToken(String accessToken, Exchange exchange) t 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); @@ -416,6 +434,57 @@ private void validateAudienceFromIntrospection( 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 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 userPermissions; @@ -440,6 +509,16 @@ private void validatePermissions(String accessToken, Exchange exchange) throws E 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 90639e8374d19..539417472e9bd 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 @@ void testParseAndVerifyAccessTokenAcceptsValidToken() throws Exception { 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 c3c1c2f8f2696..c13a39723e0b6 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 @@ public KeycloakTokenIntrospector getTokenIntrospector() { 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"); + } }