Skip to content
Open
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 @@ -469,67 +469,132 @@ protected boolean validateToken(final HttpServletRequest request, final HttpServ
final String tokenId = TokenUtils.getTokenId(token);
final String displayableTokenId = Tokens.getTokenIDDisplayText(tokenId);
final String displayableToken = Tokens.getTokenDisplayText(token.toString());
// confirm that issuer matches the intended target
if (expectedIssuers.contains(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)) {
boolean audValid = validateAudiences(token);
if (audValid) {
Date nbf = token.getNotBeforeDate();
if (nbf == null || new Date().after(nbf)) {
final TokenMetadata tokenMetadata = tokenStateService == null ? null : tokenStateService.getTokenMetadata(tokenId);
if (isTokenEnabled(tokenMetadata)) {
if (isIdleTimeoutLimitNotExceeded(tokenMetadata)) {
if (verifyTokenSignature(token)) {
markLastUsedAt(tokenId, tokenMetadata);
return true;
} else {
log.failedToVerifyTokenSignature(displayableToken, displayableTokenId);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
}
// Issuer in the static trusted list: full validation using the provider-configured
// PEM/JWKS/instance-key chain. An empty set signals "use verifyTokenSignature()".
return doFullTokenValidation(request, response, token, tokenId,
displayableToken, displayableTokenId, Set.of());
}
// For issuers not in the static list, subclasses may resolve JWKS for a runtime-registered issuer.
// An empty result means "not applicable for this request" and the token is rejected.
// All other validation checks (expiry, audiences, nbf, token state) run identically to the static path.
final Set<URI> registeredIssuerJwks = resolveRegisteredIssuerJwks(token.getIssuer(), request);
if (!registeredIssuerJwks.isEmpty()) {
return doFullTokenValidation(request, response, token, tokenId,
displayableToken, displayableTokenId, registeredIssuerJwks);
}
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
return false;
}

/**
* Extension point for subclasses to resolve JWKS for an issuer that is registered at runtime
* (e.g., in {@code TrustedOidcIssuerService}) but is not in the static
* {@code jwt.expected.issuer} topology parameter.
*
* <p>Return semantics:
* <ul>
* <li>Non-empty set — caller runs full token validation using only these JWKS for signature
* verification; the provider-configured PEM/JWKS/instance-key chain is not consulted.</li>
* <li>Empty set — not applicable for this request; caller rejects with 401.</li>
* </ul>
*
* <p>The default implementation always returns an empty set. Subclasses that support a runtime
* issuer registry should override this method, applying any request-context checks themselves,
* and return a non-empty set only when the issuer is found in the registry and
* its JWKS URI has been successfully resolved.
*/
protected Set<URI> resolveRegisteredIssuerJwks(String issuer, HttpServletRequest request) {
return Set.of();
}

/**
* Runs the full token validation sequence (expiry, audiences, nbf, token state, signature)
* used by both the static-issuer path and the registered-issuer path.
*
* @param registeredIssuerJwks if non-empty, the signature is verified exclusively against these
* JWKS URIs (resolved for the issuer from the runtime registry); if empty,
* {@link #verifyTokenSignature(JWT)} is used instead (provider-configured PEM / JWKS /
* instance-key chain).
*/
private boolean doFullTokenValidation(final HttpServletRequest request, final HttpServletResponse response,
final JWT token, final String tokenId, final String displayableToken,
final String displayableTokenId, final Set<URI> registeredIssuerJwks)
throws IOException, ServletException {
try {
if (tokenIsStillValid(token)) {
if (validateAudiences(token)) {
Date nbf = token.getNotBeforeDate();
if (nbf == null || new Date().after(nbf)) {
final TokenMetadata tokenMetadata = tokenStateService == null ? null : tokenStateService.getTokenMetadata(tokenId);
if (isTokenEnabled(tokenMetadata)) {
if (isIdleTimeoutLimitNotExceeded(tokenMetadata)) {
final boolean sigOk = registeredIssuerJwks.isEmpty()
? verifyTokenSignature(token)
: verifyTokenSignatureWithJwks(token, registeredIssuerJwks);
if (sigOk) {
markLastUsedAt(tokenId, tokenMetadata);
return true;
} else {
log.idleTimoutExceeded(token.getSubject(), displayableTokenId, idleTimeoutSeconds);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, TOKEN_PREFIX + displayableTokenId + IDLE_TIMEOUT_POSTFIX);
log.failedToVerifyTokenSignature(displayableToken, displayableTokenId);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, null);
}
} else {
log.disabledToken(displayableTokenId);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, TOKEN_PREFIX + displayableTokenId + DISABLED_POSTFIX);
log.idleTimoutExceeded(token.getSubject(), displayableTokenId, idleTimeoutSeconds);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED,
TOKEN_PREFIX + displayableTokenId + IDLE_TIMEOUT_POSTFIX);
}
} else {
log.notBeforeCheckFailed();
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: the NotBefore check failed");
log.disabledToken(displayableTokenId);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED,
TOKEN_PREFIX + displayableTokenId + DISABLED_POSTFIX);
}
} else {
log.failedToValidateAudience(displayableToken, displayableTokenId);
log.notBeforeCheckFailed();
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: missing required token audience");
"Bad request: the NotBefore check failed");
}
} else {
log.tokenHasExpired(displayableToken, displayableTokenId);

// Explicitly evict the record of this token's signature verification (if present).
// There is no value in keeping this record for expired tokens, and explicitly removing them may prevent
// records for other valid tokens from being prematurely evicted from the cache.
removeSignatureVerificationRecord(token.toString());

handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, "Token has expired");

log.failedToValidateAudience(displayableToken, displayableTokenId);
handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"Bad request: missing required token audience");
}
} catch (UnknownTokenException e) {
log.unableToVerifyExpiration(e);
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
} else {
log.tokenHasExpired(displayableToken, displayableTokenId);
// Explicitly evict the record of this token's signature verification (if present).
// There is no value in keeping this record for expired tokens, and explicitly removing them
// may prevent records for other valid tokens from being prematurely evicted from the cache.
removeSignatureVerificationRecord(token.toString());
handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, "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());
}

return false;
}

/**
* Verifies the token's signature against the given JWKS URIs.
* Uses the filter's configured signature algorithm and JWS type verifier.
*/
private boolean verifyTokenSignatureWithJwks(final JWT token, final Set<URI> jwksUrls) {
final String serializedJWT = token.toString();
if (hasSignatureBeenVerified(serializedJWT)) {
return true;
}
try {
final boolean verified = authority.verifyToken(token, jwksUrls, expectedSigAlg, typeVerifier);
if (verified) {
recordSignatureVerification(serializedJWT);
}
return verified;
} catch (TokenServiceException e) {
log.unableToVerifyToken(e);
return false;
}
}

private boolean isTokenEnabled(TokenMetadata tokenMetadata) throws UnknownTokenException {
return tokenMetadata == null ? true : tokenMetadata.isEnabled();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.security.TokenExchangePrincipalImpl;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
Expand All @@ -44,13 +47,16 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.text.ParseException;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static java.nio.charset.StandardCharsets.UTF_8;
Expand Down Expand Up @@ -249,6 +255,10 @@ private static void addKnoxIDFAttributes(ServletRequest request, JWT token) {
if (scope != null) {
request.setAttribute(KnoxIDFConstants.SCOPE_ATTRIBUTE, token.getClaim(scope));
}
final String issuer = token.getIssuer();
if (issuer != null) {
request.setAttribute(KnoxIDFConstants.TOKEN_ISS_ATTRIBUTE, issuer);
}
}

private void validateClientID(HttpServletRequest request, String tokenValue) {
Expand Down Expand Up @@ -579,6 +589,33 @@ private Subject createSubjectForTokenExchange(JWT subjectToken, JWT actorToken)
return new Subject(true, principals, emptySet, emptySet);
}

@Override
protected Set<URI> resolveRegisteredIssuerJwks(String issuer, HttpServletRequest request) {
if (!TOKEN_EXCHANGE.equals(request.getParameter(GRANT_TYPE))) {
return Set.of();
}
final GatewayServices gws = (GatewayServices)
request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
if (gws != null) {
final TrustedOidcIssuerService issuerSvc = gws.getService(ServiceType.TRUSTED_OIDC_ISSUER_SERVICE);
// isDynamicJwks() is the combined guard: true only if the issuer is both registered as
// trusted AND configured for dynamic JWKS discovery. If the issuer is not registered, or
// registered without dynamic JWKS, it is not actionable through this path.
if (issuerSvc != null && issuerSvc.isDynamicJwks(issuer)) {
// resolveJwksUri() performs OIDC discovery
final Optional<String> jwksUri = issuerSvc.resolveJwksUri(issuer);
if (jwksUri.isPresent()) {
try {
return Set.of(new URI(jwksUri.get()));
} catch (URISyntaxException e) {
LOGGER.unableToVerifyToken(e);
}
}
}
}
return Set.of();
}

@Override
protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status,
String error) throws IOException {
Expand Down
Loading