From c85fa10957fcc578a3ece9186892d69fa8efb3b8 Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Mon, 30 Mar 2026 10:45:24 +0530 Subject: [PATCH 1/5] RANGER-5533:Validating ISS claim in JWT --- .../handler/jwt/RangerJwtAuthHandler.java | 49 ++++++++++++++++++- .../web/filter/RangerJwtAuthFilter.java | 1 + .../filter/RangerSSOAuthenticationFilter.java | 1 + .../TestRangerSSOAuthenticationFilter.java | 5 ++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java index 53c5aaae2c..378e295ad0 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java @@ -52,11 +52,13 @@ public abstract class RangerJwtAuthHandler implements RangerAuthHandler { public static final String KEY_JWT_PUBLIC_KEY = "jwt.public-key"; // JWT token provider public key public static final String KEY_JWT_COOKIE_NAME = "jwt.cookie-name"; // JWT cookie name public static final String KEY_JWT_AUDIENCES = "jwt.audiences"; + public static final String KEY_JWT_ISS = "jwt.issuers"; public static final String JWT_AUTHZ_PREFIX = "Bearer "; protected static String cookieName = "hadoop-jwt"; protected List audiences; + protected List issuers; protected JWKSource keySource; private JWSVerifier verifier; private String jwksProviderUrl; @@ -99,6 +101,12 @@ public void initialize(final Properties config) throws Exception { audiences = Arrays.asList(audiencesStr.split(",")); } + // setup issuers if configured + String issuersStr = config.getProperty(KEY_JWT_ISS); + if (StringUtils.isNotBlank(issuersStr)) { + issuers = Arrays.asList(issuersStr.split(",")); + } + if (LOG.isDebugEnabled()) { LOG.debug("<<<=== RangerJwtAuthHandler.initialize()"); } @@ -182,20 +190,25 @@ protected boolean validateToken(final SignedJWT jwtToken) { boolean expValid = validateExpiration(jwtToken); boolean sigValid = false; boolean audValid = false; + boolean issValid = false; if (expValid) { sigValid = validateSignature(jwtToken); if (sigValid) { audValid = validateAudiences(jwtToken); + + if (audValid) { + issValid = validateIssuer(jwtToken); + } } } if (LOG.isDebugEnabled()) { - LOG.debug("expValid={}, sigValid={}, audValid={}", expValid, sigValid, audValid); + LOG.debug("expValid={}, sigValid={}, audValid={}, issValid={}", expValid, sigValid, audValid, issValid); } - return sigValid && audValid && expValid; + return sigValid && audValid && expValid && issValid; } /** @@ -290,6 +303,38 @@ protected boolean validateAudiences(final SignedJWT jwtToken) { return valid; } + /** + * Validate whether any of the accepted issuer claims is present in the issued + * token claims list for issuer. Override this method in subclasses in order + * to customize the audience validation behavior. + * + * @param jwtToken the JWT token from which the JWT issuer will be obtained + * @return true if an expected issuer is present, otherwise false + */ + protected boolean validateIssuer(final SignedJWT jwtToken) { + boolean valid = false; + try { + String tokenIssuer = jwtToken.getJWTClaimsSet().getIssuer(); + // if there were no expected issuers configured then just consider any issuer acceptable + if (issuers == null) { + valid = true; + } else { + // if any of the configured issuers is found then consider it acceptable + if (issuers.contains(tokenIssuer)) { + if (LOG.isDebugEnabled()) { + LOG.debug("JWT token issuer has been successfully validated."); + } + valid = true; + } else { + LOG.warn("JWT issuer validation failed."); + } + } + } catch (ParseException pe) { + LOG.warn("Unable to parse the JWT token.", pe); + } + return valid; + } + /** * Validate that the expiration time of the JWT has not been violated. If * it has, then throw an AuthenticationException. Override this method in diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java index a43cc30ced..25928f1cb8 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java @@ -73,6 +73,7 @@ public void initialize() { config.setProperty(RangerJwtAuthHandler.KEY_JWT_PUBLIC_KEY, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_PUBLIC_KEY, "")); config.setProperty(RangerJwtAuthHandler.KEY_JWT_COOKIE_NAME, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_COOKIE_NAME, RangerSSOAuthenticationFilter.JWT_COOKIE_NAME_DEFAULT)); config.setProperty(RangerJwtAuthHandler.KEY_JWT_AUDIENCES, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_AUDIENCES, "")); + config.setProperty(RangerJwtAuthHandler.KEY_JWT_ISS, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_ISSUERS, "")); super.initialize(config); } catch (Exception e) { diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java index b2879e4d17..f991680f01 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java @@ -80,6 +80,7 @@ public class RangerSSOAuthenticationFilter implements Filter { public static final String JWT_PUBLIC_KEY = "ranger.sso.publicKey"; public static final String JWT_COOKIE_NAME = "ranger.sso.cookiename"; public static final String JWT_AUDIENCES = "ranger.sso.audiences"; + public static final String JWT_ISSUERS = "ranger.sso.issuers"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM = "ranger.sso.query.param.originalurl"; public static final String JWT_COOKIE_NAME_DEFAULT = "hadoop-jwt"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT = "originalUrl"; diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java index 38459cd669..58baa63831 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java @@ -498,4 +498,9 @@ public void testDoFilter_ssoDisabled_locallogin_redirectsToLoginJsp() throws Exc verify(res, times(1)).sendRedirect("/login.jsp"); verify(chain, times(0)).doFilter(any(ServletRequest.class), any(ServletResponse.class)); } + + @Test + void testJwtIssuersConstant() { + assertEquals("ranger.sso.issuers", RangerSSOAuthenticationFilter.JWT_ISSUERS); + } } From 5dd1ce0f842930b5cf59e5c3963fe95a31f8377d Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Wed, 8 Apr 2026 11:54:47 +0530 Subject: [PATCH 2/5] RANGER-5533: Refactor JWT issuer validation to support single-issuer config --- .../authz/handler/jwt/RangerJwtAuthHandler.java | 16 ++++++++-------- .../security/web/filter/RangerJwtAuthFilter.java | 2 +- .../filter/RangerSSOAuthenticationFilter.java | 2 +- .../TestRangerSSOAuthenticationFilter.java | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java index 378e295ad0..71922fce12 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java @@ -52,13 +52,13 @@ public abstract class RangerJwtAuthHandler implements RangerAuthHandler { public static final String KEY_JWT_PUBLIC_KEY = "jwt.public-key"; // JWT token provider public key public static final String KEY_JWT_COOKIE_NAME = "jwt.cookie-name"; // JWT cookie name public static final String KEY_JWT_AUDIENCES = "jwt.audiences"; - public static final String KEY_JWT_ISS = "jwt.issuers"; + public static final String KEY_JWT_ISS = "jwt.issuer"; public static final String JWT_AUTHZ_PREFIX = "Bearer "; protected static String cookieName = "hadoop-jwt"; protected List audiences; - protected List issuers; + protected String issuer; protected JWKSource keySource; private JWSVerifier verifier; private String jwksProviderUrl; @@ -102,9 +102,9 @@ public void initialize(final Properties config) throws Exception { } // setup issuers if configured - String issuersStr = config.getProperty(KEY_JWT_ISS); - if (StringUtils.isNotBlank(issuersStr)) { - issuers = Arrays.asList(issuersStr.split(",")); + String issuerStr = config.getProperty(KEY_JWT_ISS); + if (StringUtils.isNotBlank(issuerStr)) { + issuer = issuerStr.trim(); } if (LOG.isDebugEnabled()) { @@ -315,12 +315,12 @@ protected boolean validateIssuer(final SignedJWT jwtToken) { boolean valid = false; try { String tokenIssuer = jwtToken.getJWTClaimsSet().getIssuer(); - // if there were no expected issuers configured then just consider any issuer acceptable - if (issuers == null) { + // if no expected issuer is configured then consider any issuer acceptable + if (StringUtils.isBlank(issuer)) { valid = true; } else { // if any of the configured issuers is found then consider it acceptable - if (issuers.contains(tokenIssuer)) { + if (issuer.equals(tokenIssuer)) { if (LOG.isDebugEnabled()) { LOG.debug("JWT token issuer has been successfully validated."); } diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java index 25928f1cb8..7039934e80 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java @@ -73,7 +73,7 @@ public void initialize() { config.setProperty(RangerJwtAuthHandler.KEY_JWT_PUBLIC_KEY, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_PUBLIC_KEY, "")); config.setProperty(RangerJwtAuthHandler.KEY_JWT_COOKIE_NAME, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_COOKIE_NAME, RangerSSOAuthenticationFilter.JWT_COOKIE_NAME_DEFAULT)); config.setProperty(RangerJwtAuthHandler.KEY_JWT_AUDIENCES, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_AUDIENCES, "")); - config.setProperty(RangerJwtAuthHandler.KEY_JWT_ISS, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_ISSUERS, "")); + config.setProperty(RangerJwtAuthHandler.KEY_JWT_ISS, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_ISSUER, "")); super.initialize(config); } catch (Exception e) { diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java index f991680f01..d5ff86db04 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java @@ -80,7 +80,7 @@ public class RangerSSOAuthenticationFilter implements Filter { public static final String JWT_PUBLIC_KEY = "ranger.sso.publicKey"; public static final String JWT_COOKIE_NAME = "ranger.sso.cookiename"; public static final String JWT_AUDIENCES = "ranger.sso.audiences"; - public static final String JWT_ISSUERS = "ranger.sso.issuers"; + public static final String JWT_ISSUER = "ranger.sso.issuer"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM = "ranger.sso.query.param.originalurl"; public static final String JWT_COOKIE_NAME_DEFAULT = "hadoop-jwt"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT = "originalUrl"; diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java index 58baa63831..1ba6cebd7b 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java @@ -501,6 +501,6 @@ public void testDoFilter_ssoDisabled_locallogin_redirectsToLoginJsp() throws Exc @Test void testJwtIssuersConstant() { - assertEquals("ranger.sso.issuers", RangerSSOAuthenticationFilter.JWT_ISSUERS); + assertEquals("ranger.sso.issuer", RangerSSOAuthenticationFilter.JWT_ISSUER); } } From ca45df4e8afe551694b812494f61e8953d152166 Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Thu, 9 Apr 2026 20:18:05 +0530 Subject: [PATCH 3/5] RANGER-5533: Addressed review comments --- .../handler/jwt/RangerJwtAuthHandler.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java index 71922fce12..acfd86b99f 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java @@ -304,9 +304,9 @@ protected boolean validateAudiences(final SignedJWT jwtToken) { } /** - * Validate whether any of the accepted issuer claims is present in the issued - * token claims list for issuer. Override this method in subclasses in order - * to customize the audience validation behavior. + * Validate whether issuer present in token matches configured issuer + * Override this method in subclasses in order + * to customize the issuer validation behavior. * * @param jwtToken the JWT token from which the JWT issuer will be obtained * @return true if an expected issuer is present, otherwise false @@ -315,19 +315,12 @@ protected boolean validateIssuer(final SignedJWT jwtToken) { boolean valid = false; try { String tokenIssuer = jwtToken.getJWTClaimsSet().getIssuer(); - // if no expected issuer is configured then consider any issuer acceptable - if (StringUtils.isBlank(issuer)) { + // accept if no issuer was configured or the present issuer matches the configured issuer + if (StringUtils.isBlank(issuer) || issuer.equals(tokenIssuer)) { valid = true; + LOG.debug("JWT token issuer has been successfully validated."); } else { - // if any of the configured issuers is found then consider it acceptable - if (issuer.equals(tokenIssuer)) { - if (LOG.isDebugEnabled()) { - LOG.debug("JWT token issuer has been successfully validated."); - } - valid = true; - } else { - LOG.warn("JWT issuer validation failed."); - } + LOG.warn("JWT issuer validation failed."); } } catch (ParseException pe) { LOG.warn("Unable to parse the JWT token.", pe); From 13bb8db0c715a6bca9df574338e2588555645a62 Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Mon, 13 Apr 2026 04:54:23 +0530 Subject: [PATCH 4/5] RANGER-5533 : Unit test for RangerJwtAuthHandler --- .../handler/jwt/RangerJwtAuthHandler.java | 2 +- .../handler/jwt/TestRangerJwtAuthHandler.java | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 ranger-authn/src/test/java/org/apache/ranger/authz/handler/jwt/TestRangerJwtAuthHandler.java diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java index acfd86b99f..bf31117f5d 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java @@ -101,7 +101,7 @@ public void initialize(final Properties config) throws Exception { audiences = Arrays.asList(audiencesStr.split(",")); } - // setup issuers if configured + // setup issuer if configured String issuerStr = config.getProperty(KEY_JWT_ISS); if (StringUtils.isNotBlank(issuerStr)) { issuer = issuerStr.trim(); diff --git a/ranger-authn/src/test/java/org/apache/ranger/authz/handler/jwt/TestRangerJwtAuthHandler.java b/ranger-authn/src/test/java/org/apache/ranger/authz/handler/jwt/TestRangerJwtAuthHandler.java new file mode 100644 index 0000000000..4841a28fab --- /dev/null +++ b/ranger-authn/src/test/java/org/apache/ranger/authz/handler/jwt/TestRangerJwtAuthHandler.java @@ -0,0 +1,109 @@ +/* + * 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.ranger.authz.handler.jwt; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import org.apache.ranger.authz.handler.RangerAuth; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.HttpServletRequest; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestRangerJwtAuthHandler { + static class TestHandler extends RangerJwtAuthHandler { + @Override + public ConfigurableJWTProcessor getJwtProcessor(JWSKeySelector keySelector) { + return null; + } + + @Override + public RangerAuth authenticate(HttpServletRequest request) { + return null; + } + + boolean callValidateIssuer(SignedJWT jwt) { + return validateIssuer(jwt); + } + } + + private static SignedJWT jwtWithIssuer(String issuer) { + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(issuer) + .subject("user") + .expirationTime(new Date(System.currentTimeMillis() + 60_000)) + .build(); + + // Header alg value doesn't matter for validateIssuer() + return new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claims); + } + + @Test + void validateIssuerTrue_whenIssuerNotConfigured() { + TestHandler handler = new TestHandler(); + handler.issuer = null; // StringUtils.isBlank(null) => true + + SignedJWT jwt = jwtWithIssuer("any-issuer"); + + assertTrue(handler.callValidateIssuer(jwt)); + } + + @Test + void validateIssuerTrue_whenIssuerMatches() { + TestHandler handler = new TestHandler(); + handler.issuer = "expected-issuer"; + + SignedJWT jwt = jwtWithIssuer("expected-issuer"); + + assertTrue(handler.callValidateIssuer(jwt)); + } + + @Test + void validateIssuerFalse_whenIssuerDoesNotMatch() { + TestHandler handler = new TestHandler(); + handler.issuer = "expected-issuer"; + + SignedJWT jwt = jwtWithIssuer("different-issuer"); + + assertFalse(handler.callValidateIssuer(jwt)); + } + + @Test + void validateIssuerFalse_whenJwtClaimsCannotBeParsed() throws Exception { + TestHandler handler = new TestHandler(); + handler.issuer = "expected-issuer"; + + String header = "eyJhbGciOiJIUzI1NiJ9"; // Header: {"alg":"HS256"} (valid JWS header for SignedJWT) + String payload = "buyevwv678"; // Payload: "not-json" (NOT a JSON object => getJWTClaimsSet() will throw ParseException) + String signature = "abcd"; // Signature: "sig" (any base64url string works for parsing) + + SignedJWT badJwt = SignedJWT.parse(header + "." + payload + "." + signature); + + assertFalse(handler.callValidateIssuer(badJwt)); + } +} From ddd267de4869c8f8c6b730f3f9547aa9dcdfa14e Mon Sep 17 00:00:00 2001 From: ChinmayHegde24 Date: Tue, 14 Apr 2026 11:24:09 +0530 Subject: [PATCH 5/5] RANGER-5533: Removed redundant test case from TestRangerSSOAuthenticationFilter --- .../web/filter/TestRangerSSOAuthenticationFilter.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java index 1ba6cebd7b..38459cd669 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSSOAuthenticationFilter.java @@ -498,9 +498,4 @@ public void testDoFilter_ssoDisabled_locallogin_redirectsToLoginJsp() throws Exc verify(res, times(1)).sendRedirect("/login.jsp"); verify(chain, times(0)).doFilter(any(ServletRequest.class), any(ServletResponse.class)); } - - @Test - void testJwtIssuersConstant() { - assertEquals("ranger.sso.issuer", RangerSSOAuthenticationFilter.JWT_ISSUER); - } }