This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit 542fb11789555484f4ae6d14be1ef890d41a0227 Author: lmccay <[email protected]> AuthorDate: Tue Jun 16 11:12:59 2026 -0400 KNOX-3347 - Introduce TokenExchangePrincipal for extending Act claim … (#1262) * KNOX-3347 - Introduce TokenExchangePrincipal for extending Act claim for token_exchange (cherry picked from commit 5d410895d452a6fe8b270a59ffce8d3a48237160) --- .../filter/AbstractIdentityAssertionFilter.java | 23 ++++ .../federation/jwt/filter/JWTFederationFilter.java | 151 ++++++++++++++++++++- .../apache/knox/gateway/security/SubjectUtils.java | 17 +++ .../gateway/security/TokenExchangePrincipal.java | 122 +++++++++++++++++ .../security/TokenExchangePrincipalImpl.java | 108 +++++++++++++++ 5 files changed, 416 insertions(+), 5 deletions(-) diff --git a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java index 4c212a534..81efb1511 100644 --- a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java +++ b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java @@ -51,6 +51,7 @@ import org.apache.knox.gateway.security.GroupPrincipal; import org.apache.knox.gateway.security.ImpersonatedPrincipal; import org.apache.knox.gateway.security.PrimaryPrincipal; import org.apache.knox.gateway.security.SubjectUtils; +import org.apache.knox.gateway.security.TokenExchangePrincipal; import org.apache.knox.gateway.security.TokenIdPrincipal; public abstract class AbstractIdentityAssertionFilter extends @@ -106,6 +107,23 @@ public abstract class AbstractIdentityAssertionFilter extends throw new IllegalStateException("Required Subject Missing"); } + // RFC 8693: Check if this is a token exchange request + // For token exchange, we need to apply local policy mapping to the subject identity + // BEFORE using it for impersonation. This ensures topology configuration takes precedence. + TokenExchangePrincipal tokenExchangePrincipal = SubjectUtils.getTokenExchangePrincipal(currentSubject); + if (tokenExchangePrincipal != null) { + // Get the subject identity from token exchange + String tokenExchangeSubject = tokenExchangePrincipal.getSubjectPrincipalName(); + + // Apply local policy mapping to the subject identity + // This allows topology configuration to map external identities to local ones + String mappedTokenExchangeSubject = mapUserPrincipal(tokenExchangeSubject); + + // Use the mapped subject as the identity to impersonate + // This respects local policy while honoring the token exchange semantics + mappedPrincipalName = mappedTokenExchangeSubject; + } + String primaryPrincipalName = SubjectUtils.getPrimaryPrincipalName(currentSubject); if (primaryPrincipalName != null) { if (!primaryPrincipalName.equals(mappedPrincipalName)) { @@ -153,6 +171,11 @@ public abstract class AbstractIdentityAssertionFilter extends final Set<ActorChainPrincipal> actorChainPrincipals = SubjectUtils.getActorChainPrincipal(currentSubject, subject); subject.getPrincipals().addAll(actorChainPrincipals); + // RFC 8693 Token Exchange: Preserve TokenExchangePrincipal for audit trail + if (tokenExchangePrincipal != null) { + subject.getPrincipals().add(tokenExchangePrincipal); + } + doAs(request, response, chain, subject); } else { diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java index ed207f86f..378c16a98 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java @@ -21,7 +21,10 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; +import org.apache.knox.gateway.security.ActorChainPrincipalImpl; import org.apache.knox.gateway.security.PrimaryPrincipal; +import org.apache.knox.gateway.security.TokenExchangePrincipal; +import org.apache.knox.gateway.security.TokenExchangePrincipalImpl; 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; @@ -42,11 +45,13 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +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.Set; import static java.nio.charset.StandardCharsets.UTF_8; @@ -68,6 +73,7 @@ public class JWTFederationFilter extends AbstractJWTFilter { public static final String REFRESH_TOKEN_PARAM = "refresh_token"; public static final String TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"; public static final String SUBJECT_TOKEN = "subject_token"; + public static final String ACTOR_TOKEN = "actor_token"; public static final String CLIENT_ASSERTION_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type"; public static final String CLIENT_ASSERTION = "client_assertion"; @@ -175,6 +181,15 @@ public class JWTFederationFilter extends AbstractJWTFilter { } } + // RFC 8693: Check if this is a token exchange request + HttpServletRequest httpRequest = (HttpServletRequest) request; + String grantType = httpRequest.getParameter(GRANT_TYPE); + if (TOKEN_EXCHANGE.equals(grantType)) { + // Handle RFC 8693 token exchange with subject_token and actor_token + handleTokenExchange(httpRequest, (HttpServletResponse) response, chain); + return; + } + Pair<TokenType, String> wireToken = null; try { wireToken = getWireToken(request); @@ -189,9 +204,9 @@ public class JWTFederationFilter extends AbstractJWTFilter { if (TokenType.JWT.equals(tokenType)) { try { - JWT token = new JWTToken(tokenValue); - if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, token)) { - final Subject subject = createSubjectFromToken(token); + JWT token = parseAndValidateJWT((HttpServletRequest) request, (HttpServletResponse) response, chain, tokenValue); + if (token != null) { + Subject subject = createSubjectFromToken(token); addKnoxIDFAttributes(request, token); continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } @@ -396,8 +411,8 @@ public class JWTFederationFilter extends AbstractJWTFilter { final List<Cookie> relevantCookies = CookieUtils.getCookiesForName(request, cookieName); for (Cookie ssoCookie : relevantCookies) { try { - final JWT token = new JWTToken(ssoCookie.getValue()); - if (validateToken(request, response, chain, token)) { + final JWT token = parseAndValidateJWT(request, response, chain, ssoCookie.getValue()); + if (token != null) { final Subject subject = createSubjectFromToken(token); continueWithEstablishedSecurityContext(subject, request, response, chain); // we found a valid cookie we don't need to keep checking anymore @@ -418,6 +433,132 @@ public class JWTFederationFilter extends AbstractJWTFilter { return false; } + /** + * Handle RFC 8693 token exchange flow. + * + * <p>This method validates both the subject_token and actor_token parameters, + * creates a TokenExchangePrincipal with the identity information from both tokens, + * and establishes a Subject with the actor as the PrimaryPrincipal.</p> + * + * <p>The TokenExchangePrincipal signals to the identity assertion layer that + * impersonation should be established with the subject as the ImpersonatedPrincipal.</p> + * + * @param request the HTTP request containing subject_token and actor_token parameters + * @param response the HTTP response + * @param chain the filter chain + * @throws IOException if an I/O error occurs + * @throws ServletException if a servlet error occurs + */ + private void handleTokenExchange(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Extract subject_token (required) + String subjectTokenValue = request.getParameter(SUBJECT_TOKEN); + if (subjectTokenValue == null || subjectTokenValue.isEmpty()) { + handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST, + "RFC 8693 token exchange requires subject_token parameter"); + return; + } + + // Extract actor_token (required for proper token exchange) + String actorTokenValue = request.getParameter(ACTOR_TOKEN); + if (actorTokenValue == null || actorTokenValue.isEmpty()) { + handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST, + "RFC 8693 token exchange requires actor_token parameter"); + return; + } + + try { + // Parse and validate subject_token + JWT subjectToken = parseAndValidateJWT(request, response, chain, subjectTokenValue); + if (subjectToken == null) { + // Validation failed, error response already sent + return; + } + + // Parse and validate actor_token + JWT actorToken = parseAndValidateJWT(request, response, chain, actorTokenValue); + if (actorToken == null) { + // Validation failed, error response already sent + return; + } + + // Create Subject with actor as PrimaryPrincipal and TokenExchangePrincipal + Subject subject = createSubjectForTokenExchange(subjectToken, actorToken); + + continueWithEstablishedSecurityContext(subject, request, response, chain); + + } catch (ParseException e) { + LOGGER.failedToParsePasscodeToken(e); + handleValidationError(request, response, HttpServletResponse.SC_UNAUTHORIZED, + "Failed to parse token in token exchange: " + e.getMessage()); + } + } + + /** + * Parse and validate a JWT token. + * + * @param request the HTTP request + * @param response the HTTP response + * @param chain the filter chain + * @param tokenValue the JWT string to parse + * @return the parsed and validated JWT, or null if validation failed + * @throws ParseException if the JWT cannot be parsed + * @throws IOException if an I/O error occurs during validation + * @throws ServletException if a servlet error occurs during validation + */ + private JWT parseAndValidateJWT(HttpServletRequest request, HttpServletResponse response, + FilterChain chain, String tokenValue) + throws ParseException, IOException, ServletException { + JWT token = new JWTToken(tokenValue); + if (validateToken(request, response, chain, token)) { + return token; + } + // Validation failed - error response already sent by validateToken + return null; + } + + /** + * Create a Subject for RFC 8693 token exchange with proper principal setup. + * + * @param subjectToken the validated subject token + * @param actorToken the validated actor token + * @return a Subject configured for token exchange + */ + private Subject createSubjectForTokenExchange(JWT subjectToken, JWT actorToken) { + // Extract identities from the tokens + String subjectPrincipalName = subjectToken.getSubject(); + String subjectIssuer = subjectToken.getIssuer(); + String actorPrincipalName = actorToken.getSubject(); + String actorIssuer = actorToken.getIssuer(); + // Create principals for the Subject + // PrimaryPrincipal is the ACTOR (the authenticated party) + PrimaryPrincipal primaryPrincipal = + new PrimaryPrincipal(actorPrincipalName); + + // TokenExchangePrincipal carries metadata for identity assertion layer + TokenExchangePrincipal tokenExchangePrincipal = + new TokenExchangePrincipalImpl( + subjectPrincipalName, subjectIssuer, actorPrincipalName, actorIssuer); + + // Extract actor chain from subject_token (if present) using existing logic + List<Map<String, Object>> actorChain = + TokenUtils.extractActorChain(subjectToken); + + // Create Subject with all necessary principals + Set<Principal> principals = new HashSet<>(); + principals.add(primaryPrincipal); + principals.add(tokenExchangePrincipal); + + // Add ActorChainPrincipal if actor chain exists in subject_token + if (!actorChain.isEmpty()) { + principals.add(new ActorChainPrincipalImpl(actorChain)); + } + + @SuppressWarnings("rawtypes") + HashSet emptySet = new HashSet(); + return new Subject(true, principals, emptySet, emptySet); + } + @Override protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status, String error) throws IOException { diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java index 11db9113f..9b6b50a62 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java @@ -105,4 +105,21 @@ public class SubjectUtils { public static Set<ActorChainPrincipal> getActorChainPrincipal(Subject currentSubject, Subject subject) { return currentSubject.getPrincipals(ActorChainPrincipal.class); } + + /** + * Get the TokenExchangePrincipal from the subject if present. + * + * <p>This is used to detect when an RFC 8693 token exchange has occurred + * and to extract the subject and actor identities.</p> + * + * @param subject the subject to check + * @return the TokenExchangePrincipal if present, null otherwise + */ + public static TokenExchangePrincipal getTokenExchangePrincipal(Subject subject) { + if (subject == null) { + return null; + } + Set<TokenExchangePrincipal> principals = subject.getPrincipals(TokenExchangePrincipal.class); + return principals.isEmpty() ? null : principals.iterator().next(); + } } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipal.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipal.java new file mode 100644 index 000000000..835fa1450 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipal.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.security; + +import java.security.Principal; + +/** + * Principal that represents an RFC 8693 token exchange operation. + * + * <p>This principal carries metadata about both the subject and actor tokens + * through the authentication and identity assertion pipeline. It signals to + * downstream layers that a token exchange has occurred and provides the + * necessary information to establish proper impersonation semantics.</p> + * + * <p>In RFC 8693 token exchange:</p> + * <ul> + * <li><b>Subject Token</b>: Represents the identity of the party on behalf + * of whom the request is being made (will become ImpersonatedPrincipal)</li> + * <li><b>Actor Token</b>: Represents the identity of the acting party who + * is authorized to act (will become PrimaryPrincipal)</li> + * </ul> + * + * <p>This principal is added to the Subject during authentication/federation + * and is used by the identity assertion layer to establish the correct + * impersonation relationship using Subject.doAs().</p> + * + * <p>Example usage flow:</p> + * <pre> + * // In JWTFederationFilter (authentication layer): + * JWT subjectToken = validateSubjectToken(request); + * JWT actorToken = validateActorToken(request); + * TokenExchangePrincipal tep = new TokenExchangePrincipalImpl( + * subjectToken.getSubject(), subjectToken.getIssuer(), + * actorToken.getSubject(), actorToken.getIssuer() + * ); + * subject.getPrincipals().add(tep); + * + * // In AbstractIdentityAssertionFilter (identity assertion layer): + * TokenExchangePrincipal tep = getTokenExchangePrincipal(subject); + * if (tep != null) { + * // Set up impersonation: actor acts as subject + * mappedPrincipalName = tep.getSubjectPrincipalName(); + * // This triggers existing impersonation logic + * } + * </pre> + * + * @see <a href="https://datatracker.ietf.org/doc/html/rfc8693#section-2.1">RFC 8693 Section 2.1</a> + */ +public interface TokenExchangePrincipal extends Principal { + + /** + * Get the subject principal name (the identity on behalf of whom the request is made). + * + * <p>In the token exchange flow, this is extracted from the subject_token parameter. + * This identity will typically become the {@link ImpersonatedPrincipal} after the + * identity assertion layer processes the token exchange.</p> + * + * @return the subject principal name, never null + */ + String getSubjectPrincipalName(); + + /** + * Get the issuer of the subject token. + * + * <p>Combined with the subject principal name, this provides a globally unique + * identity for the subject. This is important for audit trails and security + * decisions where the issuer context matters.</p> + * + * @return the subject token issuer, may be null if not available + */ + String getSubjectIssuer(); + + /** + * Get the actor principal name (the identity of the acting party). + * + * <p>In the token exchange flow, this is extracted from the actor_token parameter. + * This identity represents the authenticated party who is authorized to act on + * behalf of the subject. This will typically be the {@link PrimaryPrincipal}.</p> + * + * @return the actor principal name, never null + */ + String getActorPrincipalName(); + + /** + * Get the issuer of the actor token. + * + * <p>Combined with the actor principal name, this provides a globally unique + * identity for the actor. This is important for audit trails and security + * decisions where the issuer context matters.</p> + * + * @return the actor token issuer, may be null if not available + */ + String getActorIssuer(); + + /** + * Returns the actor principal name. + * + * <p>This implements {@link Principal#getName()} and returns the actor's identity + * since the actor is the authenticated party performing the token exchange.</p> + * + * @return the actor principal name + */ + @Override + default String getName() { + return getActorPrincipalName(); + } +} diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipalImpl.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipalImpl.java new file mode 100644 index 000000000..01cfb48f1 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/TokenExchangePrincipalImpl.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.security; + +import java.util.Objects; + +/** + * Implementation of TokenExchangePrincipal that holds RFC 8693 token exchange metadata. + */ +public class TokenExchangePrincipalImpl implements TokenExchangePrincipal { + private final String subjectPrincipalName; + private final String subjectIssuer; + private final String actorPrincipalName; + private final String actorIssuer; + + /** + * Creates a new TokenExchangePrincipal with the specified subject and actor identities. + * + * @param subjectPrincipalName the subject principal name (required) + * @param subjectIssuer the subject token issuer (may be null) + * @param actorPrincipalName the actor principal name (required) + * @param actorIssuer the actor token issuer (may be null) + * @throws IllegalArgumentException if subjectPrincipalName or actorPrincipalName is null + */ + public TokenExchangePrincipalImpl(String subjectPrincipalName, String subjectIssuer, + String actorPrincipalName, String actorIssuer) { + if (subjectPrincipalName == null || subjectPrincipalName.isEmpty()) { + throw new IllegalArgumentException("Subject principal name cannot be null or empty"); + } + if (actorPrincipalName == null || actorPrincipalName.isEmpty()) { + throw new IllegalArgumentException("Actor principal name cannot be null or empty"); + } + this.subjectPrincipalName = subjectPrincipalName; + this.subjectIssuer = subjectIssuer; + this.actorPrincipalName = actorPrincipalName; + this.actorIssuer = actorIssuer; + } + + @Override + public String getSubjectPrincipalName() { + return subjectPrincipalName; + } + + @Override + public String getSubjectIssuer() { + return subjectIssuer; + } + + @Override + public String getActorPrincipalName() { + return actorPrincipalName; + } + + @Override + public String getActorIssuer() { + return actorIssuer; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("TokenExchangePrincipal[actor="); + sb.append(actorPrincipalName); + if (actorIssuer != null) { + sb.append("@").append(actorIssuer); + } + sb.append(", subject=").append(subjectPrincipalName); + if (subjectIssuer != null) { + sb.append("@").append(subjectIssuer); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TokenExchangePrincipal)) { + return false; + } + TokenExchangePrincipal that = (TokenExchangePrincipal) o; + return Objects.equals(subjectPrincipalName, that.getSubjectPrincipalName()) + && Objects.equals(subjectIssuer, that.getSubjectIssuer()) + && Objects.equals(actorPrincipalName, that.getActorPrincipalName()) + && Objects.equals(actorIssuer, that.getActorIssuer()); + } + + @Override + public int hashCode() { + return Objects.hash(subjectPrincipalName, subjectIssuer, actorPrincipalName, actorIssuer); + } +}
