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

This commit is contained in:
Mauro Mugnaini 2024-04-30 18:29:21 +02:00
parent 8c009b9a8d
commit 7d98fbaa16
Signed by: mauro.mugnaini
GPG Key ID: 2440CFD0EB321EA8
2 changed files with 112 additions and 20 deletions

View File

@ -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 <code>true</code> if the token is valid, <code>false</code> 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 <code>false</code> token expiration check is disabled
* @return <code>true</code> if the token is valid, <code>false</code> 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 <code>true</code> if the token is valid, <code>false</code> 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 <code>false</code> token expiration check is disabled
* @return <code>true</code> if the token is valid, <code>false</code> 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 <code>false</code> token expiration check is disabled
* @return <code>true</code> if the token is valid, <code>false</code> 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;
}

View File

@ -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