Skip to content

Commit

Permalink
Add new method related to tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
drighetto committed Feb 9, 2025
1 parent 624a0ae commit b68c1d4
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 46 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
<artifactId>commons-imaging</artifactId>
<version>1.0.0-alpha5</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.5.0</version>
</dependency>
<!-- TEST ONLY PURPOSE -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/eu/righettod/SecurityUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.righettod;


import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.imaging.ImageInfo;
Expand Down Expand Up @@ -1232,4 +1233,51 @@ public static boolean isXMLHaveCommentsOrXSLProcessingInstructions(String xmlFil
}
return itemsDetected;
}


/**
* Perform a set of additional validations against a JWT token:
* <ul>
* <li>Match the expected type of token: ACCESS or ID or REFRESH.</li>
* <li>The token ID (<a href="https://www.iana.org/assignments/jwt/jwt.xhtml">JTI claim</a>) is NOT part of the list of revoked token.</li>
* </ul>
*
* @param token JWT token for which <b>signature was already validated</b> and on which a set of additional validations will be applied.
* @param expectedTokenType The type of expected token using the enumeration provided.
* @param revokedTokenJTIList A list of token identifier (<b>JTI</b> claim) referring to tokens that were revoked and to which the JTI claim of the token will be compared to.
* @return True only the token pass all the validations.
* @see "https://www.iana.org/assignments/jwt/jwt.xhtml"
* @see "https://auth0.com/docs/secure/tokens/access-tokens"
* @see "https://auth0.com/docs/secure/tokens/id-tokens"
* @see "https://auth0.com/docs/secure/tokens/refresh-tokens"
* @see "https://auth0.com/blog/id-token-access-token-what-is-the-difference/"
* @see "https://jwt.io/libraries?language=Java"
* @see "https://pentesterlab.com/blog/secure-jwt-library-design"
*/
public static boolean applyJWTExtraValidation(DecodedJWT token, TokenType expectedTokenType, List<String> revokedTokenJTIList) {
boolean isValid = false;
TokenType tokenType;
try {
String jti = token.getId();
if (jti != null && !jti.trim().isEmpty()) {
boolean jtiIsRevoked = revokedTokenJTIList.stream().anyMatch(jti::equalsIgnoreCase);
if (!jtiIsRevoked) {
//Determine the token type based on the presence of specifics claims
if (!token.getClaim("scope").isMissing()) {
tokenType = TokenType.ACCESS;
} else if (!token.getClaim("name").isMissing() || !token.getClaim("email").isMissing()) {
tokenType = TokenType.ID;
} else {
tokenType = TokenType.REFRESH;
}
isValid = (tokenType.equals(expectedTokenType));
}
}
} catch (Exception e) {
//In case of error then assume that the check failed
isValid = false;
}

return isValid;
}
}
28 changes: 28 additions & 0 deletions src/main/java/eu/righettod/TokenType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package eu.righettod;

/**
* Enumeration used by the method <code>SecurityUtils.applyJWTExtraValidation()</code> to define the type of token.
*/
public enum TokenType {

/**
* Access token.
*
* @see "https://auth0.com/docs/secure/tokens/access-tokens"
*/
ACCESS,

/**
* ID token.
*
* @see "https://auth0.com/docs/secure/tokens/id-tokens"
*/
ID,

/**
* Refresh token.
*
* @see "https://auth0.com/docs/secure/tokens/refresh-tokens"
*/
REFRESH
}
105 changes: 59 additions & 46 deletions src/test/java/eu/righettod/TestSecurityUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package eu.righettod;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -13,9 +18,12 @@
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -456,31 +464,12 @@ public void isEmailAddress() {
final String templateMsgFalseNegative = "Email address '%s' must be detected as invalid!";
final String templateMsgFalsePositive = "Email address '%s' must be detected as valid!";
//Test invalid email addresses
List<String> invalidEmailAddressesList = Arrays.asList(
"=?utf-8?q?=41=42=43?=test@test.com",
"=?utf-7?q?=41GYAbwBvAGIAYBy-?=@test@com",
"=?utf-8?b?Zm9vYmFy?=@test.com",
"@mail.mit.edu:peter@hotmail.com",
"peter%hotmail.com@mail.mit.edu",
"rusx!umoskva!kgbvax!dimitri@gateway.ru",
"test@example.com@evil.com",
"(foo)user@(bar)example.com",
"postmaster@[123.123.123.123]",
"postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
"foo@xn--mnchen-2ya.com");
List<String> invalidEmailAddressesList = Arrays.asList("=?utf-8?q?=41=42=43?=test@test.com", "=?utf-7?q?=41GYAbwBvAGIAYBy-?=@test@com", "=?utf-8?b?Zm9vYmFy?=@test.com", "@mail.mit.edu:peter@hotmail.com", "peter%hotmail.com@mail.mit.edu", "rusx!umoskva!kgbvax!dimitri@gateway.ru", "test@example.com@evil.com", "(foo)user@(bar)example.com", "postmaster@[123.123.123.123]", "postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]", "foo@xn--mnchen-2ya.com");
invalidEmailAddressesList.forEach(addr -> {
assertFalse(SecurityUtils.isEmailAddress(addr), String.format(templateMsgFalseNegative, addr));
});
//Test valid email addresses
List<String> validEmailAddressesList = Arrays.asList(
"test@test.com",
"test-test@test.com",
"test.test@test.com",
"test_test@test.com",
"test132@test.com",
"test+label@test.com",
"\"John..Doe\"@example.com",
"\"@\"@example.com");
List<String> validEmailAddressesList = Arrays.asList("test@test.com", "test-test@test.com", "test.test@test.com", "test_test@test.com", "test132@test.com", "test+label@test.com", "\"John..Doe\"@example.com", "\"@\"@example.com");
validEmailAddressesList.forEach(addr -> {
assertTrue(SecurityUtils.isEmailAddress(addr), String.format(templateMsgFalsePositive, addr));
});
Expand All @@ -491,13 +480,7 @@ public void isPSD2StetSafeCertificateURL() {
final String templateMsgIPFalseNegative = "URL '%s' must be detected as invalid!";
final String templateMsgIPFalsePositive = "URL '%s' must be detected as valid!";
//Test invalid urls
List<String> invalidUrls = Arrays.asList(
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582X68b17e865fede4636d726b709fX",
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f?a=b",
"http://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f",
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c99",
"https://test.com/myQsealCertificate-714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f"
);
List<String> invalidUrls = Arrays.asList("https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582X68b17e865fede4636d726b709fX", "https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f?a=b", "http://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f", "https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c99", "https://test.com/myQsealCertificate-714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f");
invalidUrls.forEach(u -> {
assertFalse(SecurityUtils.isPSD2StetSafeCertificateURL(u), String.format(templateMsgIPFalseNegative, u));
});
Expand All @@ -522,11 +505,7 @@ public void applyURLDecoding() {
assertEquals(refDecodedData, SecurityUtils.applyURLDecoding(encodedData, threshold));
});
//Test invalid cases
SecurityException thrown = assertThrows(
SecurityException.class,
() -> SecurityUtils.applyURLDecoding(testData.get(6), 3),
"SecurityException expected!"
);
SecurityException thrown = assertThrows(SecurityException.class, () -> SecurityUtils.applyURLDecoding(testData.get(6), 3), "SecurityException expected!");
assertTrue(thrown.getMessage().equalsIgnoreCase("Decoding round threshold of 3 reached!"));
}

Expand All @@ -535,25 +514,15 @@ public void isPathSafe() {
final String templateMsgFalseNegative = "Path '%s' must be detected as invalid!";
final String templateMsgFalsePositive = "Path '%s' must be detected as valid!";
//Test invalid cases
List<String> invalidPaths = Arrays.asList(
"/home/../../../../etc/password",
"%2Fhome%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpassword", //URL encoding X1
List<String> invalidPaths = Arrays.asList("/home/../../../../etc/password", "%2Fhome%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpassword", //URL encoding X1
"%252Fhome%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fpassword", //URL encoding X2
"%25252525252Fhome%25252525252F%25252525252E%25252525252E%25252525252F%25252525252E%25252525252E%25252525252F%25252525252E%25252525252E%25252525252F%25252525252E%25252525252E%25252525252Fetc%25252525252Fpassword", //URL encoding X6
"/home/..\\/..\\/..\\/..\\/etc/password",
"/home/..\\\\/..\\/..\\\\/..\\/etc/password",
"D:\\test..\\\\..\\test"
);
"/home/..\\/..\\/..\\/..\\/etc/password", "/home/..\\\\/..\\/..\\\\/..\\/etc/password", "D:\\test..\\\\..\\test");
invalidPaths.forEach(p -> {
assertFalse(SecurityUtils.isPathSafe(p), String.format(templateMsgFalseNegative, p));
});
//Test valid cases
List<String> validPaths = Arrays.asList(
"/home/file",
"C:\\test\\file",
"test/file",
"test\\file"
);
List<String> validPaths = Arrays.asList("/home/file", "C:\\test\\file", "test/file", "test\\file");
validPaths.forEach(p -> {
assertTrue(SecurityUtils.isPathSafe(p), String.format(templateMsgFalsePositive, p));
});
Expand All @@ -574,5 +543,49 @@ public void isXMLHaveCommentsOrXSLProcessingInstructions() {
result = SecurityUtils.isXMLHaveCommentsOrXSLProcessingInstructions(testFile);
assertFalse(result, "No Comments or XSL PI were expected to be detected!");
}

@Test
public void applyJWTExtraValidation() {
final String templateMsgIPFalseNegative = "Token '%s' must be detected as invalid!";
final String templateMsgIPFalsePositive = "Token '%s' must be detected as valid!";
List<String> revokedTokenJTIList = List.of("TOkEn2", "TOkEn3", "TOkEn4");
//Test invalid cases
//--Provide an ACCESS TOKEN but an ID TOKEN is expected
DecodedJWT testToken = generateJWTToken(TokenType.ACCESS, "TOKEN1");
boolean result = SecurityUtils.applyJWTExtraValidation(testToken, TokenType.ID, revokedTokenJTIList);
assertFalse(result, String.format(templateMsgIPFalseNegative, testToken.getToken()));
//--Provide the expected token but the token JTI is part of the revoked token list
testToken = generateJWTToken(TokenType.ID, "TOKEN2");
result = SecurityUtils.applyJWTExtraValidation(testToken, TokenType.ID, revokedTokenJTIList);
assertFalse(result, String.format(templateMsgIPFalseNegative, testToken.getToken()));
//---Provide the expected token but the token JTI claim is not present
testToken = generateJWTToken(TokenType.REFRESH, null);
result = SecurityUtils.applyJWTExtraValidation(testToken, TokenType.REFRESH, revokedTokenJTIList);
assertFalse(result, String.format(templateMsgIPFalseNegative, testToken.getToken()));
//Test valid cases
//--Provide the expected token and the token JTI is NOT part of the revoked token list
for (TokenType tType : TokenType.values()) {
testToken = generateJWTToken(tType, "TOKEN1");
result = SecurityUtils.applyJWTExtraValidation(testToken, tType, revokedTokenJTIList);
assertTrue(result, String.format(templateMsgIPFalsePositive, testToken.getToken()));
}
}

private DecodedJWT generateJWTToken(TokenType tokenType, String jti) {
String secret = "6dbdd2a3-c7c6-42cf-abea-ca8c20b4d536";
Instant expirationTime = Instant.now().plus(Duration.ofHours(1));
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
JWTCreator.Builder builder = JWT.create();
if (jti != null) {
builder = builder.withJWTId(jti);
}
switch (tokenType) {
case ACCESS -> builder = builder.withClaim("scope", "BUSINESS_API");
case ID -> builder = builder.withClaim("name", "test user");
}
String signedToken = builder.withExpiresAt(expirationTime).withClaim("tokenTypeHints", tokenType.toString()).sign(algorithm);
JWTVerifier verifier = JWT.require(algorithm).build();
return verifier.verify(signedToken);
}
}

0 comments on commit b68c1d4

Please sign in to comment.