Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(authentication-webhook): change way to retrieve algorithm, more … #3845

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
Loading