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..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 @@ -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.issuer"; public static final String JWT_AUTHZ_PREFIX = "Bearer "; protected static String cookieName = "hadoop-jwt"; protected List audiences; + protected String issuer; 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 issuer if configured + String issuerStr = config.getProperty(KEY_JWT_ISS); + if (StringUtils.isNotBlank(issuerStr)) { + issuer = issuerStr.trim(); + } + 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,31 @@ protected boolean validateAudiences(final SignedJWT jwtToken) { return valid; } + /** + * 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 + */ + protected boolean validateIssuer(final SignedJWT jwtToken) { + boolean valid = false; + try { + String tokenIssuer = jwtToken.getJWTClaimsSet().getIssuer(); + // 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 { + 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/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)); + } +} 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..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,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_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 b2879e4d17..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,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_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";