From 7d98fbaa1609d32de0d1ca4e28a051c27dae2013 Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Tue, 30 Apr 2024 18:29:21 +0200 Subject: [PATCH] Overloaded methods to disable token expiration, generalized public key generation providing key algorithm and added support of RS384 and RS512 signature algorithms, defaulting to RS256 if not specified --- .../common/keycloak/model/ModelUtils.java | 116 +++++++++++++++--- .../gcube/common/keycloak/TestModelUtils.java | 16 ++- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java index 820d39c..9c5c92d 100644 --- a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java +++ b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java @@ -1,6 +1,7 @@ package org.gcube.common.keycloak.model; import java.security.KeyFactory; +import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; @@ -16,7 +17,7 @@ import org.slf4j.LoggerFactory; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.JWTVerifier; /** @@ -49,13 +50,25 @@ public class ModelUtils { } /** - * Creates and {@link RSAPublicKey} instance from its string PEM representation + * Creates a {@link RSAPublicKey} instance from its string PEM representation * * @param publicKeyPem the public key PEM string * @return the RSA public key - * @throws Exception if it's not possbile to create the RSA public key from the PEM string + * @throws Exception if it's not possible to create the RSA public key from the PEM string */ public static RSAPublicKey createRSAPublicKey(String publicKeyPem) throws Exception { + return (RSAPublicKey) createPublicKey(publicKeyPem, "RSA"); + } + + /** + * Creates a {@link PublicKey} instance from its string PEM representation + * + * @param publicKeyPem the public key PEM string + * @param algorithm the key type (e.g. RSA) + * @return the public key + * @throws Exception if it's not possible to create the public key from the PEM string + */ + public static PublicKey createPublicKey(String publicKeyPem, String algorithm) throws Exception { try { String publicKey = publicKeyPem.replaceFirst("-----BEGIN (.*)-----\n", ""); publicKey = publicKey.replaceFirst("-----END (.*)-----", ""); @@ -63,10 +76,39 @@ public class ModelUtils { publicKey = publicKey.replaceAll("\n", ""); byte[] encoded = Base64.getDecoder().decode(publicKey); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded)); + KeyFactory kf = KeyFactory.getInstance(algorithm); + return kf.generatePublic(new X509EncodedKeySpec(encoded)); } catch (Exception e) { - throw new RuntimeException("Cant' create RSA public key from PEM string", e); + throw new RuntimeException("Cannot create public key from PEM string", e); + } + } + + /** + * Verifies the token validity + * + * @param token the base64 JWT token string + * @param rsaPublicKey the realm's RSA public key on server + * @return true if the token is valid, false otherwise + * @throws RuntimeException if an error occurs constructing the verifier + */ + public static boolean isValid(String token, RSAPublicKey rsaPublicKey) throws Exception { + return isValid(token, rsaPublicKey, true); + } + + /** + * Verifies the token validity + * + * @param token the base64 JWT token string + * @param rsaPublicKey the realm's RSA public key on server + * @param checkExpiration if false token expiration check is disabled + * @return true if the token is valid, false otherwise + * @throws RuntimeException if an error occurs constructing the verifier + */ + public static boolean isValid(String token, RSAPublicKey rsaPublicKey, boolean checkExpiration) throws Exception { + try { + return isValid(token, Algorithm.RSA256(rsaPublicKey, null), checkExpiration); + } catch (Exception e) { + throw new RuntimeException("Cannot construct the JWT verifier", e); } } @@ -75,23 +117,69 @@ public class ModelUtils { * * @param token the base64 JWT token string * @param publicKey the realm's public key on server + * @param keyAlgorithm the public key algorithm * @return true if the token is valid, false otherwise - * @throws RuntimeException if an error occurs constructing the digital signature verifier + * @throws RuntimeException if an error occurs constructing the verifier */ - public static boolean isValid(String token, RSAPublicKey publicKey) throws RuntimeException { - JWTVerifier verifier = null; + public static boolean isValid(String token, PublicKey publicKey, String keyAlgorithm) throws Exception { + return isValid(token, publicKey, keyAlgorithm, true); + } + + /** + * Verifies the token validity + * + * @param token the base64 JWT token string + * @param publicKey the realm's public key on server + * @param keyAlgorithm the public key algorithm + * @param checkExpiration if false token expiration check is disabled + * @return true if the token is valid, false otherwise + * @throws RuntimeException if an error occurs constructing the verifier + */ + public static boolean isValid(String token, PublicKey publicKey, String keyAlgorithm, boolean checkExpiration) throws Exception { try { - Algorithm algorithm = Algorithm.RSA256(publicKey, null); - verifier = JWT.require(algorithm).build(); + Algorithm algorithm = null; + switch (keyAlgorithm) { + case "RS256": + algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, null); + break; + case "RS384": + algorithm = Algorithm.RSA384((RSAPublicKey) publicKey, null); + break; + case "RS512": + algorithm = Algorithm.RSA512((RSAPublicKey) publicKey, null); + break; + default: + throw new RuntimeException("Unsupported key algorithm: " + algorithm); + } + + return isValid(token, algorithm, checkExpiration); } catch (Exception e) { - throw new RuntimeException("Cannot construct the JWT digital signature verifier", e); + throw new RuntimeException("Cannot construct the JWT verifier", e); } + } + + /** + * Verifies the token validity + * + * @param token the base64 JWT token string + * @param algorithm the algorithm to use for verification + * @param checkExpiration if false token expiration check is disabled + * @return true if the token is valid, false otherwise + */ + public static boolean isValid(String token, Algorithm algorithm, boolean checkExpiration) throws Exception { + JWTVerifier verifier = JWT.require(algorithm).build();; try { verifier.verify(token); return true; - } catch (JWTVerificationException e) { + } catch (TokenExpiredException e) { + // This is OK because expiration check is after the signature validation in the implementation if (logger.isDebugEnabled()) { - logger.debug("JWT digital signature is not verified", e); + logger.debug("JWT is expired: {}", e.getMessage()); + } + return !checkExpiration; + } catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("JWT is not verified: {}", e.getMessage()); } return false; } diff --git a/src/test/java/org/gcube/common/keycloak/TestModelUtils.java b/src/test/java/org/gcube/common/keycloak/TestModelUtils.java index 37f5bf0..f0398f5 100644 --- a/src/test/java/org/gcube/common/keycloak/TestModelUtils.java +++ b/src/test/java/org/gcube/common/keycloak/TestModelUtils.java @@ -3,6 +3,7 @@ package org.gcube.common.keycloak; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; +import java.security.interfaces.RSAPublicKey; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; import org.gcube.common.keycloak.model.AccessToken; @@ -33,16 +34,19 @@ public class TestModelUtils { } @Test - public void testTokenDigitalSignature() throws Exception { - logger.info("Start testing OIDC token response object binding..."); + public void testTokenValidity() throws Exception { + logger.info("Start testing access token valdity..."); TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"), TokenResponse.class); - // Valid signature - Assert.assertFalse("Token signature is valid", - ModelUtils.isValid(tr.getAccessToken(), ModelUtils.createRSAPublicKey( - new String(Files.readAllBytes(Paths.get("src/test/resources/rsa-public-key.pem")))))); + RSAPublicKey publicKey = ModelUtils.createRSAPublicKey( + new String(Files.readAllBytes(Paths.get("src/test/resources/rsa-public-key.pem")))); + // Valid signature + Assert.assertFalse("Token is valid", ModelUtils.isValid(tr.getAccessToken(), publicKey)); + Assert.assertTrue("Token is valid", ModelUtils.isValid(tr.getAccessToken(), publicKey, false)); + Assert.assertFalse("Token signature is valid", ModelUtils.isValid(tr.getAccessToken().replace("ZV9hY2Nlc3", "ZV9hY2Nlcc"), publicKey)); + Assert.assertFalse("Token is not expired", ModelUtils.isValid(tr.getAccessToken(), publicKey, true)); } @Test