Skip to content

Commit

Permalink
KNOX-2542 and KNOX-2544 (apache#409)
Browse files Browse the repository at this point in the history
* KNOX-2542 - Token-based providers should check expiration before verifying tokens

* Fix build issues

* KNOX-2544 - Token-based providers should cache successful token verifications
  • Loading branch information
pzampino authored Mar 11, 2021
1 parent 9e2ce5e commit fab8bac
Show file tree
Hide file tree
Showing 11 changed files with 4,996 additions and 4,538 deletions.
4,577 changes: 2,341 additions & 2,236 deletions gateway-admin-ui/package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private HadoopAuthFilter testIfJwtSupported(String supportJwt) throws Exception
expect(filterConfig.getInitParameter(JWTFederationFilter.TOKEN_VERIFICATION_PEM)).andReturn(null).anyTimes();
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_EXPECTED_ISSUER)).andReturn(null).anyTimes();
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_EXPECTED_SIGALG)).andReturn(null).anyTimes();
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_VERIFIED_CACHE_MAX)).andReturn(null).anyTimes();
}

final ServletContext servletContext = createMock(ServletContext.class);
Expand Down
4 changes: 4 additions & 0 deletions gateway-provider-security-jwt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,9 @@
<artifactId>slf4j-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public interface JWTMessages {
@Message( level = MessageLevel.INFO, text = "Path {0} is configured as unauthenticated path, letting the request {1} through" )
void unauthenticatedPathBypass(String path, String uri);

@Message( level = MessageLevel.WARN, text = "Unable to vderive authentication provider url: {0}" )
@Message( level = MessageLevel.WARN, text = "Unable to derive authentication provider URL: {0}" )
void failedToDeriveAuthenticationProviderUrl(@StackTrace( level = MessageLevel.ERROR) Exception e);

@Message( level = MessageLevel.ERROR,
text = "The configuration value ({0}) for maximum token verification cache is invalid; Using the default value." )
void invalidVerificationCacheMaxConfiguration(String value);

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditContext;
Expand Down Expand Up @@ -81,6 +83,9 @@ public abstract class AbstractJWTFilter implements Filter {
public static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
public static final String JWT_DEFAULT_SIGALG = "RS256";

public static final String JWT_VERIFIED_CACHE_MAX = "jwt.verified.cache.max";
public static final int JWT_VERIFIED_CACHE_MAX_DEFAULT = 100;

static JWTMessages log = MessagesFactory.get( JWTMessages.class );
private static AuditService auditService = AuditServiceFactory.getAuditService();
private static Auditor auditor = auditService.getAuditor(
Expand All @@ -90,6 +95,7 @@ public abstract class AbstractJWTFilter implements Filter {
protected List<String> audiences;
protected JWTokenAuthority authority;
protected RSAPublicKey publicKey;
protected Cache<String, Boolean> verifiedTokens;
private String expectedIssuer;
private String expectedSigAlg;
protected String expectedPrincipalClaim;
Expand Down Expand Up @@ -120,6 +126,29 @@ public void init( FilterConfig filterConfig ) throws ServletException {
}
}
}

// Setup the verified tokens cache
initializeVerifiedTokensCache(filterConfig);
}

/**
* Initialize the cache for token verifications records.
*
* @param config The filter configuration
*/
private void initializeVerifiedTokensCache(final FilterConfig config) {
int maxCacheSize = JWT_VERIFIED_CACHE_MAX_DEFAULT;

String configValue = config.getInitParameter(JWT_VERIFIED_CACHE_MAX);
if (configValue != null && !configValue.isEmpty()) {
try {
maxCacheSize = Integer.parseInt(configValue);
} catch (NumberFormatException e) {
log.invalidVerificationCacheMaxConfiguration(configValue);
}
}

verifiedTokens = Caffeine.newBuilder().maximumSize(maxCacheSize).build();
}

protected void configureExpectedParameters(FilterConfig filterConfig) {
Expand Down Expand Up @@ -261,78 +290,115 @@ protected Subject createSubjectFromToken(JWT token) {
protected boolean validateToken(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, JWT token)
throws IOException, ServletException {
boolean verified = false;
try {
if (publicKey != null) {
verified = authority.verifyToken(token, publicKey);
} else if (expectedJWKSUrl != null) {
verified = authority.verifyToken(token, expectedJWKSUrl, expectedSigAlg);
} else {
verified = authority.verifyToken(token);
}
} catch (TokenServiceException e) {
log.unableToVerifyToken(e);
}

// Check received signature algorithm if expectation is configured
if (verified && expectedSigAlg != null) {
try {
final String receivedSigAlg = JWSHeader.parse(token.getHeader()).getAlgorithm().getName();
if (!receivedSigAlg.equals(expectedSigAlg)) {
verified = false;
}
} catch (ParseException e) {
log.unableToVerifyToken(e);
verified = false;
}
}

final String tokenId = TokenUtils.getTokenId(token);
final String displayableToken = Tokens.getTokenDisplayText(token.toString());
if (verified) {
// confirm that issue matches intended target
if (expectedIssuer.equals(token.getIssuer())) {
// if there is no expiration data then the lifecycle is tied entirely to
// the cookie validity - otherwise ensure that the current time is before
// the designated expiration time
try {
if (tokenIsStillValid(token)) {
// confirm that issuer matches the intended target
if (expectedIssuer.equals(token.getIssuer())) {
// if there is no expiration data then the lifecycle is tied entirely to
// the cookie validity - otherwise ensure that the current time is before
// the designated expiration time
try {
if (tokenIsStillValid(token)) {
// Verify the token signature
if (verifyToken(token)) {
boolean audValid = validateAudiences(token);
if (audValid) {
Date nbf = token.getNotBeforeDate();
if (nbf == null || new Date().after(nbf)) {
return true;
} else {
log.notBeforeCheckFailed();
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: the NotBefore check failed");
}
}
else {
Date nbf = token.getNotBeforeDate();
if (nbf == null || new Date().after(nbf)) {
return true;
} else {
log.notBeforeCheckFailed();
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: the NotBefore check failed");
}
} else {
log.failedToValidateAudience(tokenId, displayableToken);
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: missing required token audience");
"Bad request: missing required token audience");
}
} else {
log.tokenHasExpired(tokenId, displayableToken);
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: token has expired");
log.failedToVerifyTokenSignature(tokenId, displayableToken);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
}
} catch (UnknownTokenException e) {
log.unableToVerifyExpiration(e);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
} else {
log.tokenHasExpired(tokenId, displayableToken);
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: token has expired");
}
} else {
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
} catch (UnknownTokenException e) {
log.unableToVerifyExpiration(e);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
} else {
log.failedToVerifyTokenSignature(tokenId, displayableToken);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
}

return false;
}

protected boolean verifyToken(final JWT token) {
boolean verified;

String tokenId = TokenUtils.getTokenId(token);

// Check if the token has already been verified
verified = hasTokenBeenVerified(tokenId);

// If it has not yet been verified, then perform the verification now
if (!verified) {
try {
if (publicKey != null) {
verified = authority.verifyToken(token, publicKey);
} else if (expectedJWKSUrl != null) {
verified = authority.verifyToken(token, expectedJWKSUrl, expectedSigAlg);
} else {
verified = authority.verifyToken(token);
}
} catch (TokenServiceException e) {
log.unableToVerifyToken(e);
}

// Check received signature algorithm if expectation is configured
if (verified && expectedSigAlg != null) {
try {
final String receivedSigAlg = JWSHeader.parse(token.getHeader()).getAlgorithm().getName();
if (!receivedSigAlg.equals(expectedSigAlg)) {
verified = false;
}
} catch (ParseException e) {
log.unableToVerifyToken(e);
verified = false;
}
}

if (verified) { // If successful, record the verification for future reference
recordTokenVerification(tokenId);
}
}

return verified;
}

/**
* Determine if the specified token has previously been successfully verified.
*
* @param tokenId The unique identifier for a token.
*
* @return true, if the specified token has been previously verified; Otherwise, false.
*/
protected boolean hasTokenBeenVerified(final String tokenId) {
return (verifiedTokens.getIfPresent(tokenId) != null);
}

/**
* Record a successful token verification.
*
* @param tokenId The unique identifier for the token which has been successfully verified.
*/
protected void recordTokenVerification(final String tokenId) {
verifiedTokens.put(tokenId, true);
}

protected abstract void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status,
String error) throws IOException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@

public class JWTFederationFilter extends AbstractJWTFilter {

public static final String KNOX_TOKEN_AUDIENCES = "knox.token.audiences";
public static final String TOKEN_VERIFICATION_PEM = "knox.token.verification.pem";
public static final String KNOX_TOKEN_QUERY_PARAM_NAME = "knox.token.query.param.name";
public static final String TOKEN_PRINCIPAL_CLAIM = "knox.token.principal.claim";
public static final String JWKS_URL = "knox.token.jwks.url";
private static final String BEARER = "Bearer ";
private static final String BASIC = "Basic";
private static final String TOKEN = "Token";
private String paramName;
public static final String KNOX_TOKEN_AUDIENCES = "knox.token.audiences";
public static final String TOKEN_VERIFICATION_PEM = "knox.token.verification.pem";
public static final String KNOX_TOKEN_QUERY_PARAM_NAME = "knox.token.query.param.name";
public static final String TOKEN_PRINCIPAL_CLAIM = "knox.token.principal.claim";
public static final String JWKS_URL = "knox.token.jwks.url";
private static final String BEARER = "Bearer ";
private static final String BASIC = "Basic";
private static final String TOKEN = "Token";
private String paramName;

@Override
public void init( FilterConfig filterConfig ) throws ServletException {
Expand Down Expand Up @@ -154,4 +154,5 @@ protected void handleValidationError(HttpServletRequest request, HttpServletResp
response.sendError(status);
}
}

}
Loading

0 comments on commit fab8bac

Please sign in to comment.