Skip to content

Commit

Permalink
feat(authentication-webhook): change way to retrieve algorithm, more … (
Browse files Browse the repository at this point in the history
#3845)

* feat(authentication-webhook): change way to retrieve algorithm, more flexible

* feat(authentication-webhook): change way to retrieve algorithm, more flexible 2

* feat(authentication-webhook): change way to retrieve algorithm, more flexible 3
  • Loading branch information
mathias-vandaele authored Jan 13, 2025
1 parent e175ed5 commit dfd774f
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.camunda.connector.inbound.authorization.AuthorizationResult.Success;
import io.camunda.connector.inbound.model.JWTProperties;
import io.camunda.connector.inbound.model.WebhookAuthorization.JwtAuth;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
Expand All @@ -39,7 +40,10 @@
final class JWTAuthHandler extends WebhookAuthorizationHandler<JwtAuth> {

private static final Logger LOGGER = LoggerFactory.getLogger(JWTAuthHandler.class);

private static final AuthorizationResult JWT_AUTH_FAILED_RESULT =
new InvalidCredentials("JWT auth failed");
private static final AuthorizationResult JWT_AUTH_MISSING_PERMISSIONS_RESULT =
new Forbidden("Missing required permissions");
private final JwkProvider jwkProvider;
private final ObjectMapper objectMapper;

Expand All @@ -49,29 +53,6 @@ public JWTAuthHandler(JwtAuth authorization, JwkProvider jwkProvider, ObjectMapp
this.objectMapper = objectMapper;
}

@Override
public AuthorizationResult checkAuthorization(WebhookProcessingPayload payload) {

JWTProperties jwtProperties = expectedAuthorization.jwt();
Map<String, String> headers = payload.headers();

Optional<DecodedJWT> decodedJWT = getDecodedVerifiedJWT(headers, jwkProvider);
if (decodedJWT.isEmpty()) {
return JWT_AUTH_FAILED_RESULT;
}
if (jwtProperties.requiredPermissions() != null
&& !jwtProperties.requiredPermissions().isEmpty()) {

List<String> roles = extractRoles(jwtProperties, decodedJWT.get(), objectMapper);
if (!roles.containsAll(jwtProperties.requiredPermissions())) {
LOGGER.debug("JWT auth failed");
return JWT_AUTH_MISSING_PERMISSIONS_RESULT;
}
}
LOGGER.debug("JWT auth was successful");
return Success.INSTANCE;
}

private static Optional<DecodedJWT> getDecodedVerifiedJWT(
Map<String, String> headers, JwkProvider jwkProvider) {
final String jwtToken =
Expand Down Expand Up @@ -127,23 +108,23 @@ private static Optional<String> extractJWTFomHeader(final Map<String, String> he
private static DecodedJWT verifyJWT(String jwtToken, JwkProvider jwkProvider)
throws SignatureVerificationException, TokenExpiredException {
DecodedJWT verifiedJWT =
Optional.ofNullable(JWT.decode(jwtToken))
Optional.of(JWT.decode(jwtToken))
.map(
decodedJWT -> {
try {
return jwkProvider.get(decodedJWT.getKeyId());
} catch (JwkException e) {
LOGGER.warn("Cannot find JWK for the JWT token: " + e.getMessage());
throw new RuntimeException(e);
}
})
.map(
jwk -> {
try {
return JWT.require(getAlgorithm(jwk)).build();
Jwk jwk = jwkProvider.get(decodedJWT.getKeyId());
return JWT.require(
getAlgorithm(
Optional.ofNullable(jwk.getAlgorithm())
.orElse(decodedJWT.getAlgorithm()),
jwk.getPublicKey()))
.build();
} catch (InvalidPublicKeyException e) {
LOGGER.warn("Token verification failed: " + e.getMessage());
throw new RuntimeException(e);
} catch (JwkException e) {
LOGGER.warn("Cannot find JWK for the JWT token: " + e.getMessage());
throw new RuntimeException(e);
}
})
.map(jwtVerifier -> jwtVerifier.verify(jwtToken))
Expand All @@ -152,20 +133,39 @@ private static DecodedJWT verifyJWT(String jwtToken, JwkProvider jwkProvider)
return verifiedJWT;
}

private static Algorithm getAlgorithm(Jwk jwk) throws InvalidPublicKeyException {
return switch (jwk.getAlgorithm()) {
case "RS256" -> Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey());
case "RS384" -> Algorithm.RSA384((RSAPublicKey) jwk.getPublicKey());
case "RS512" -> Algorithm.RSA512((RSAPublicKey) jwk.getPublicKey());
case "ES256" -> Algorithm.ECDSA256((ECPublicKey) jwk.getPublicKey(), null);
case "ES384" -> Algorithm.ECDSA384((ECPublicKey) jwk.getPublicKey(), null);
case "ES512" -> Algorithm.ECDSA512((ECPublicKey) jwk.getPublicKey(), null);
default -> throw new RuntimeException("Unknown algorithm!");
private static Algorithm getAlgorithm(String algorithm, PublicKey publicKey)
throws InvalidPublicKeyException {
return switch (algorithm) {
case "RS256" -> Algorithm.RSA256((RSAPublicKey) publicKey);
case "RS384" -> Algorithm.RSA384((RSAPublicKey) publicKey);
case "RS512" -> Algorithm.RSA512((RSAPublicKey) publicKey);
case "ES256" -> Algorithm.ECDSA256((ECPublicKey) publicKey, null);
case "ES384" -> Algorithm.ECDSA384((ECPublicKey) publicKey, null);
case "ES512" -> Algorithm.ECDSA512((ECPublicKey) publicKey, null);
default -> throw new RuntimeException("Unknown algorithm: " + algorithm);
};
}

private static final AuthorizationResult JWT_AUTH_FAILED_RESULT =
new InvalidCredentials("JWT auth failed");
private static final AuthorizationResult JWT_AUTH_MISSING_PERMISSIONS_RESULT =
new Forbidden("Missing required permissions");
@Override
public AuthorizationResult checkAuthorization(WebhookProcessingPayload payload) {

JWTProperties jwtProperties = expectedAuthorization.jwt();
Map<String, String> headers = payload.headers();

Optional<DecodedJWT> decodedJWT = getDecodedVerifiedJWT(headers, jwkProvider);
if (decodedJWT.isEmpty()) {
return JWT_AUTH_FAILED_RESULT;
}
if (jwtProperties.requiredPermissions() != null
&& !jwtProperties.requiredPermissions().isEmpty()) {

List<String> roles = extractRoles(jwtProperties, decodedJWT.get(), objectMapper);
if (!roles.containsAll(jwtProperties.requiredPermissions())) {
LOGGER.debug("JWT auth failed");
return JWT_AUTH_MISSING_PERMISSIONS_RESULT;
}
}
LOGGER.debug("JWT auth was successful");
return Success.INSTANCE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,20 @@

public class JWTAuthHandlerTest {

private final ObjectMapper objectMapper;

private final FeelEngineWrapper feelEngineWrapper;

private static final String JWT_TOKEN =
"eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImM2ZjgzODZkMzFiOThiNzdkODNiYmEzNWE0NTdhZWY0In0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6ImFwaTEiLCJzdWIiOiI1YmU4NjM1OTA3M2M0MzRiYWQyZGEzOTMyMjIyZGFiZSIsImNsaWVudF9pZCI6Im15X2NsaWVudF9hcHAiLCJleHAiOjE3ODY4MjI2MTYsImlhdCI6MTY4NjgxOTAxNiwianRpIjoiMTE0ZjhjODRjNTM3MDNhYzIxMjBkMzAyNjExZTM1OGMiLCJyb2xlcyI6WyJhZG1pbiIsInN1cGVyYWRtaW4iXSwiYWRtaW4iOnRydWV9.KsjyrTJdpJnnji3c57wkc6REMl-501n2Nn98xd_2wZSGwpzHtf1ocsouudJ7hm-4W1dLUHJTLYJAO9thzWtH1Yomyq029ffz5CU8B7gtcrqg9OP_QuVCOcb9KPzjA_Lc5s4SELzDrJoedR90W-nL_7BYPvhrhu9dZcH3NbcaeU_531Yqc-YhVByBX_f6MwnpXYJECNGIx9F70SHrEI58paa8KLCvDu5Kcps480YsYHKCo9k5LoSmcDBGG_-n0riWfei0wGCFcHdhdI6ag08-C109oh7Po-PQ7GVTkEJ4pFmQ7dxBxsq_X39jh8w_9XynqbTaQhbwfNZ5u0SLWEp-n2yzxYFMLONI0VtSxw4zUfMUMJFW4iZvduxe_Ui4Jlj4ZmVxa60l7Wb3k4fi6C5-3hXOvb1XngFElSdFvIC2WGlaIfDfb82Bzq41PJc8Fqm3VRVWN7y5gpADT_Y9PYvZWP98AmogEMR_-l7gCr5ICDRlDpoNcCv3vVbJ6rTLvkAC";

private static final String JWT_WITH_ES512_ALGORITHM_TOKEN =
"eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzUxMiIsImtpZCI6ImZjYWYxMGJlMzdhZDhiNzQ2MWY4ZGFhYjZkMzkwYzcwIn0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6ImFwaTEiLCJzdWIiOiI1YmU4NjM1OTA3M2M0MzRiYWQyZGEzOTMyMjIyZGFiZSIsImNsaWVudF9pZCI6Im15X2NsaWVudF9hcHAiLCJleHAiOjE3ODY4MjI2MTYsImlhdCI6MTY4NjgxOTAxNiwianRpIjoiMTE0ZjhjODRjNTM3MDNhYzIxMjBkMzAyNjExZTM1OGMiLCJyb2xlcyI6WyJhZG1pbiIsInN1cGVyYWRtaW4iXSwiYWRtaW4iOnRydWV9.AGpm312EBWHyjLDh3nd6hyKQ3xQDJCpTwYYbufQ_ZQzT0URFC24TeR_8Rc-ITrCIv6sSc1JoNFUdEt1PEpiiZ1mZAN0X4LGlrDUVvIgAR2YJbc9vFSCn7rGHvN6gQjKXYB8ZTjYefgqSusGugdKwTolpKQP-Zm_C3vp-S5cUG0oGWejX";

private static final String WRONG_JWT_TOKEN =
"eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjNjZDljMWM4NDU3ZjRkMDhiNDlkMDI2OGNhNWYwMDhiIn0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6ImFwaTEiLCJzdWIiOiI1YmU4NjM1OTA3M2M0MzRiYWQyZGEzOTMyMjIyZGFiZSIsImNsaWVudF9pZCI6Im15X2NsaWVudF9hcHAiLCJleHAiOjE2ODc3OTM4MzUsImlhdCI6MTY4Nzc5MDIzNSwianRpIjoiNmE3ZDllNDljNWViZjYzNWM2MjVjNWQwZDAxOGNmYjIiLCJyb2xlcyI6WyJhZG1pbiIsInN1cGVyYWRtaW4iXSwiYWRtaW4iOnRydWV9.YP4Zw8graOY5wMJpxIZzYNN01xtOquWzT74boxMkhCdKMU_35PCoufZqUbyvNTD5YLltBe_dYe-sLuN4s-ZjeivL4ySSDtaeCd60D5JnjLq7vuC6MUd9nBHo2fIbIAwkEiWi_flCCiyzNa3Ir4KPCWxEL2cdibnjxeovUKBhnjRdf3tq4ADWrczHpf4wxZXL8aLEHzM6I5nSV6I3R9Arb6Cie-gHDfwxjGB_PoD3L5syB7izdNAMJPLlv4XHwIZ_5Pdsle546cwaZqJhmEjjHgsRJ_JEa_Xpm1zfmShHCDixkEKGfQ0JN5nYqE2JCnhlpjyWNrkqMmnAxb1AsDzwrA";

private static final String EXPIRED_JWT_TOKEN =
"eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImM2ZjgzODZkMzFiOThiNzdkODNiYmEzNWE0NTdhZWY0In0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6ImFwaTEiLCJzdWIiOiI1YmU4NjM1OTA3M2M0MzRiYWQyZGEzOTMyMjIyZGFiZSIsImNsaWVudF9pZCI6Im15X2NsaWVudF9hcHAiLCJleHAiOjE2ODcwMDAwMDAsImlhdCI6MTY4NzAwMDAwMCwianRpIjoiZTg0MWU1NzczZmUxN2ExNTYzNTM0ZWFhODRkOTNiNGQiLCJyb2xlcyI6WyJhZG1pbiIsInN1cGVyYWRtaW4iXSwiYWRtaW4iOnRydWV9.e0w7LwLKIpeXnms1eUHuNysoqxPzvhreVLKBhtOpRgiFr60Nrmn04EXEU4YdzGW4zU9tDdc9z8xTyfouQ7ImcLAj7p74v3fsIpckHwaAvi9FRu0kPVrCsmNC8a9M7pwRJsPPCi8DReQVnR0G0mTF12m9SIIpdf6VfaJeuNsHhQB5on6md4uxZ7X5fXZz3Z9A5xp3ZjPji6nknZUyTyTNcJ_GvEzZ4Jx9svHOm6OpDjVM57D8WI_6YNwqnEMQs-JxYNoWBSoIm1V_0rvMxLltINv0G6kvHjDApxcyUAbarpYVUUe0Sm2CoefNVXZPbb-X5gabqGrlKCFOf9ovprZ9NbgpHGawrhUgrJ3-ltkwwpi4zs7i0kj3iuGBRPh_8qJhH5NRvuPJVWN4RUhnuLuxhjenbE9UGPjIkqgYdWUHQ19qCVhf52m3UdHRatKG0GG1DLH4BEDZysvpa9y112oHSvWRmIasJMC3r4hrXnV1iLLIqZz7lv3UfTtXJAjqwGyY";

private static final String NOT_ENOUGH_PERMISSION_JWT_TOKEN =
"eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImM2ZjgzODZkMzFiOThiNzdkODNiYmEzNWE0NTdhZWY0In0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6ImFwaTEiLCJzdWIiOiI1YmU4NjM1OTA3M2M0MzRiYWQyZGEzOTMyMjIyZGFiZSIsImNsaWVudF9pZCI6Im15X2NsaWVudF9hcHAiLCJleHAiOjE3ODY4MjI2MTYsImlhdCI6MTY4NjgxOTAxNiwianRpIjoiMTE0ZjhjODRjNTM3MDNhYzIxMjBkMzAyNjExZTM1OGMiLCJyb2xlcyI6WyJ1c2VyIl19.Tcicz2XdMXI4Kios6fqND_gmg5DdD0U_VdraGSh6Qylv_PJYWCDXsGCbt7GofDMYML60tqikj-LvWPeZ7O-rS8Fy0jke3a866AAj1PA0Pbf1_jYJIMdiuhK1F983RDRNPNVSjPWPqKWftmDAX-S1_k2zmX3yUPakFzlAvtF7emue9K-lueJwi3x_0raq3k4YtQYfqV9Dt9kDv-S51wjnvnhJSaKu77uYYZjH92ud-OVh-AkBoH7XC6-W3WUpKXKpGQO4QkeVnTSAuXOMLw9Yn1v-rtiS0zJ9WknyydAeg9KTLZtORjgXji4QR1VqCoCxt3LvHA7PHNuIevDw4L5aMdNMRMpN0urCAegoPWYQ011n15yMD_7GfC4wlDK9XyNsWjilVoxoZIP8QhZh1IoH1XDd3YjbmIFC04yYmV-jNRS8TOzrvd4iQOmKzT7E4n58JNB9OWONKYDMbtihSy9zCpufOfVjUmItBxYFJyd_sWtOtN3gtL4Ru6Y_IKa8Ahdm";
private static final String NO_ALG_PRESENT_JWT =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6InoxcnNZSEhKOS04bWdndDRIc1p1OEJLa0JQdyIsImtpZCI6InoxcnNZSEhKOS04bWdndDRIc1p1OEJLa0JQdyJ9.eyJhdWQiOiJhcGk6Ly83YWJlOGQzNi1iMDViLTQ1OGItOTdkNy0zYjhiM2VjOWM4ZTkiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC84ZWJlMjQ5ZC04MzEyLTRmZmItOWI2Yi0wOGU1NjY2OWQ1NzgvIiwiaWF0IjoxNzM2NzYzMDk4LCJuYmYiOjE3MzY3NjMwOTgsImV4cCI6MTczNjc2Njk5OCwiYWlvIjoiazJSZ1lJaHp2Tzg4MldoMnBwV0M3cjdyaVpsbUFBPT0iLCJhcHBpZCI6IjdhYmU4ZDM2LWIwNWItNDU4Yi05N2Q3LTNiOGIzZWM5YzhlOSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzhlYmUyNDlkLTgzMTItNGZmYi05YjZiLTA4ZTU2NjY5ZDU3OC8iLCJvaWQiOiIxOTZkYzU0NC1kMzAyLTQxYmQtYjJiMS04ODE0YWUzNmRmZmEiLCJyaCI6IjEuQVRrQW5TUy1qaEtELTAtYmF3amxabW5WZURhTnZucGJzSXRGbDljN2l6N0p5T2s1QUFBNUFBLiIsInN1YiI6IjE5NmRjNTQ0LWQzMDItNDFiZC1iMmIxLTg4MTRhZTM2ZGZmYSIsInRpZCI6IjhlYmUyNDlkLTgzMTItNGZmYi05YjZiLTA4ZTU2NjY5ZDU3OCIsInV0aSI6InZkaDlRQjBySEVlUm1fZFZ4VkVLQUEiLCJ2ZXIiOiIxLjAifQ.kAEtEYbMD47IyhgZL8KDX1I65j7gPtjXdL9iv4JwcCwTx8NL0R1gKHZPvWyyg09XqyQxVF8m5r0SxXVhvaGZCbMDrkaGOKDlwNjTzQIta3gtCiLxHdsmbrMAOt8ktVGRHLKzQcvYpVSUJhSxX4XikqugusNlU1acvKWUgkzal98YF-RvwcqlevkbHeyYmaful-6gP9Yf7p4mawlupOzl_A30Qf13a07kH-39CO5H2z_akA1eB0u8sINY-Y8l0We0ncKmP-C0vlQe5T2z3vyWuTPESRtCWgXipuYzzD1T9ZupkMTa72DAWhCOLCHKeuckLTajhj_9ZmfWRkePZUC0SQ";
private final ObjectMapper objectMapper;
private final FeelEngineWrapper feelEngineWrapper;

/* JWT token content *
{
Expand Down Expand Up @@ -222,6 +218,22 @@ public void jwtCheckWithOutRoles() {
assertThat(verificationResult).isInstanceOf(Success.class);
}

@Test
public void noAlgProvidedByJwkProvider() {
// given jwt, check only signature
JwkProvider jwkProvider = new JwkProviderNoAlg();
JWTProperties jwtProperties = new JWTProperties("https://mockUrl.com", null, null);
var headers = Map.of("Authorization", "Bearer " + NO_ALG_PRESENT_JWT);
var handler = new JWTAuthHandler(new JwtAuth(jwtProperties), jwkProvider, objectMapper);
var payload = new TestWebhookProcessingPayload(headers);

// when
var verificationResult = handler.checkAuthorization(payload);

// then
assertThat(verificationResult).isInstanceOf(Success.class);
}

static class TestJwkProvider implements JwkProvider {

@Override
Expand Down Expand Up @@ -281,4 +293,19 @@ public Jwk get(String keyId) {
return Jwk.fromValues(jwkMap);
}
}

static class JwkProviderNoAlg implements JwkProvider {

@Override
public Jwk get(String keyId) {
Map<String, Object> jwkMap = new HashMap<>();
jwkMap.put("use", "sig");
jwkMap.put("kty", "RSA");
jwkMap.put(
"n",
"pOe4GbleFDT1u5ioOQjNMmhvkDVoVD9cBKvX7AlErtWA_D6wc1w1iwkd6arYVCPObZbAB4vLSXrlpBSOuP6VYnXw_cTgniv_c82ra-mfqCpM-SbqzZ3sVqlcE_bwxvci_4PrxAW4R85ok12NXyZ2371H3yGevabi35AlVm-bQ24azo1hLK_0DzB6TxsAIOTOcKfIugOfqP-B2R4vR4u6pYftS8MWcxegr9iJ5JNtubI1X2JHpxJhkRoMVwKFna2GXmtzdxLi3yS_GffVCKfTbFMhalbJS1lSmLqhmLZZL-lrQZ6fansTl1vcGcoxnzPTwBkZMks0iVV4yfym_gKBXQ");
jwkMap.put("e", "AQAB");
return Jwk.fromValues(jwkMap);
}
}
}

0 comments on commit dfd774f

Please sign in to comment.