Skip to content

Commit 669e151

Browse files
RANGER-5533:Verify JWT Issuer Claims if present (#901)
* RANGER-5533:Validating ISS claim in JWT * RANGER-5533: Refactor JWT issuer validation to support single-issuer config * RANGER-5533: Addressed review comments * RANGER-5533 : Unit test for RangerJwtAuthHandler * RANGER-5533: Removed redundant test case from TestRangerSSOAuthenticationFilter
1 parent 32bfe19 commit 669e151

4 files changed

Lines changed: 151 additions & 2 deletions

File tree

ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ public abstract class RangerJwtAuthHandler implements RangerAuthHandler {
5252
public static final String KEY_JWT_PUBLIC_KEY = "jwt.public-key"; // JWT token provider public key
5353
public static final String KEY_JWT_COOKIE_NAME = "jwt.cookie-name"; // JWT cookie name
5454
public static final String KEY_JWT_AUDIENCES = "jwt.audiences";
55+
public static final String KEY_JWT_ISS = "jwt.issuer";
5556
public static final String JWT_AUTHZ_PREFIX = "Bearer ";
5657

5758
protected static String cookieName = "hadoop-jwt";
5859

5960
protected List<String> audiences;
61+
protected String issuer;
6062
protected JWKSource<SecurityContext> keySource;
6163
private JWSVerifier verifier;
6264
private String jwksProviderUrl;
@@ -99,6 +101,12 @@ public void initialize(final Properties config) throws Exception {
99101
audiences = Arrays.asList(audiencesStr.split(","));
100102
}
101103

104+
// setup issuer if configured
105+
String issuerStr = config.getProperty(KEY_JWT_ISS);
106+
if (StringUtils.isNotBlank(issuerStr)) {
107+
issuer = issuerStr.trim();
108+
}
109+
102110
if (LOG.isDebugEnabled()) {
103111
LOG.debug("<<<=== RangerJwtAuthHandler.initialize()");
104112
}
@@ -182,20 +190,25 @@ protected boolean validateToken(final SignedJWT jwtToken) {
182190
boolean expValid = validateExpiration(jwtToken);
183191
boolean sigValid = false;
184192
boolean audValid = false;
193+
boolean issValid = false;
185194

186195
if (expValid) {
187196
sigValid = validateSignature(jwtToken);
188197

189198
if (sigValid) {
190199
audValid = validateAudiences(jwtToken);
200+
201+
if (audValid) {
202+
issValid = validateIssuer(jwtToken);
203+
}
191204
}
192205
}
193206

194207
if (LOG.isDebugEnabled()) {
195-
LOG.debug("expValid={}, sigValid={}, audValid={}", expValid, sigValid, audValid);
208+
LOG.debug("expValid={}, sigValid={}, audValid={}, issValid={}", expValid, sigValid, audValid, issValid);
196209
}
197210

198-
return sigValid && audValid && expValid;
211+
return sigValid && audValid && expValid && issValid;
199212
}
200213

201214
/**
@@ -290,6 +303,31 @@ protected boolean validateAudiences(final SignedJWT jwtToken) {
290303
return valid;
291304
}
292305

306+
/**
307+
* Validate whether issuer present in token matches configured issuer
308+
* Override this method in subclasses in order
309+
* to customize the issuer validation behavior.
310+
*
311+
* @param jwtToken the JWT token from which the JWT issuer will be obtained
312+
* @return true if an expected issuer is present, otherwise false
313+
*/
314+
protected boolean validateIssuer(final SignedJWT jwtToken) {
315+
boolean valid = false;
316+
try {
317+
String tokenIssuer = jwtToken.getJWTClaimsSet().getIssuer();
318+
// accept if no issuer was configured or the present issuer matches the configured issuer
319+
if (StringUtils.isBlank(issuer) || issuer.equals(tokenIssuer)) {
320+
valid = true;
321+
LOG.debug("JWT token issuer has been successfully validated.");
322+
} else {
323+
LOG.warn("JWT issuer validation failed.");
324+
}
325+
} catch (ParseException pe) {
326+
LOG.warn("Unable to parse the JWT token.", pe);
327+
}
328+
return valid;
329+
}
330+
293331
/**
294332
* Validate that the expiration time of the JWT has not been violated. If
295333
* it has, then throw an AuthenticationException. Override this method in
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.ranger.authz.handler.jwt;
20+
21+
import com.nimbusds.jose.JWSAlgorithm;
22+
import com.nimbusds.jose.JWSHeader;
23+
import com.nimbusds.jose.proc.JWSKeySelector;
24+
import com.nimbusds.jose.proc.SecurityContext;
25+
import com.nimbusds.jwt.JWTClaimsSet;
26+
import com.nimbusds.jwt.SignedJWT;
27+
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
28+
import org.apache.ranger.authz.handler.RangerAuth;
29+
import org.junit.jupiter.api.Test;
30+
31+
import javax.servlet.http.HttpServletRequest;
32+
33+
import java.util.Date;
34+
35+
import static org.junit.jupiter.api.Assertions.assertFalse;
36+
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
38+
public class TestRangerJwtAuthHandler {
39+
static class TestHandler extends RangerJwtAuthHandler {
40+
@Override
41+
public ConfigurableJWTProcessor<SecurityContext> getJwtProcessor(JWSKeySelector<SecurityContext> keySelector) {
42+
return null;
43+
}
44+
45+
@Override
46+
public RangerAuth authenticate(HttpServletRequest request) {
47+
return null;
48+
}
49+
50+
boolean callValidateIssuer(SignedJWT jwt) {
51+
return validateIssuer(jwt);
52+
}
53+
}
54+
55+
private static SignedJWT jwtWithIssuer(String issuer) {
56+
JWTClaimsSet claims = new JWTClaimsSet.Builder()
57+
.issuer(issuer)
58+
.subject("user")
59+
.expirationTime(new Date(System.currentTimeMillis() + 60_000))
60+
.build();
61+
62+
// Header alg value doesn't matter for validateIssuer()
63+
return new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claims);
64+
}
65+
66+
@Test
67+
void validateIssuerTrue_whenIssuerNotConfigured() {
68+
TestHandler handler = new TestHandler();
69+
handler.issuer = null; // StringUtils.isBlank(null) => true
70+
71+
SignedJWT jwt = jwtWithIssuer("any-issuer");
72+
73+
assertTrue(handler.callValidateIssuer(jwt));
74+
}
75+
76+
@Test
77+
void validateIssuerTrue_whenIssuerMatches() {
78+
TestHandler handler = new TestHandler();
79+
handler.issuer = "expected-issuer";
80+
81+
SignedJWT jwt = jwtWithIssuer("expected-issuer");
82+
83+
assertTrue(handler.callValidateIssuer(jwt));
84+
}
85+
86+
@Test
87+
void validateIssuerFalse_whenIssuerDoesNotMatch() {
88+
TestHandler handler = new TestHandler();
89+
handler.issuer = "expected-issuer";
90+
91+
SignedJWT jwt = jwtWithIssuer("different-issuer");
92+
93+
assertFalse(handler.callValidateIssuer(jwt));
94+
}
95+
96+
@Test
97+
void validateIssuerFalse_whenJwtClaimsCannotBeParsed() throws Exception {
98+
TestHandler handler = new TestHandler();
99+
handler.issuer = "expected-issuer";
100+
101+
String header = "eyJhbGciOiJIUzI1NiJ9"; // Header: {"alg":"HS256"} (valid JWS header for SignedJWT)
102+
String payload = "buyevwv678"; // Payload: "not-json" (NOT a JSON object => getJWTClaimsSet() will throw ParseException)
103+
String signature = "abcd"; // Signature: "sig" (any base64url string works for parsing)
104+
105+
SignedJWT badJwt = SignedJWT.parse(header + "." + payload + "." + signature);
106+
107+
assertFalse(handler.callValidateIssuer(badJwt));
108+
}
109+
}

security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public void initialize() {
7373
config.setProperty(RangerJwtAuthHandler.KEY_JWT_PUBLIC_KEY, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_PUBLIC_KEY, ""));
7474
config.setProperty(RangerJwtAuthHandler.KEY_JWT_COOKIE_NAME, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_COOKIE_NAME, RangerSSOAuthenticationFilter.JWT_COOKIE_NAME_DEFAULT));
7575
config.setProperty(RangerJwtAuthHandler.KEY_JWT_AUDIENCES, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_AUDIENCES, ""));
76+
config.setProperty(RangerJwtAuthHandler.KEY_JWT_ISS, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_ISSUER, ""));
7677

7778
super.initialize(config);
7879
} catch (Exception e) {

security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
8080
public static final String JWT_PUBLIC_KEY = "ranger.sso.publicKey";
8181
public static final String JWT_COOKIE_NAME = "ranger.sso.cookiename";
8282
public static final String JWT_AUDIENCES = "ranger.sso.audiences";
83+
public static final String JWT_ISSUER = "ranger.sso.issuer";
8384
public static final String JWT_ORIGINAL_URL_QUERY_PARAM = "ranger.sso.query.param.originalurl";
8485
public static final String JWT_COOKIE_NAME_DEFAULT = "hadoop-jwt";
8586
public static final String JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT = "originalUrl";

0 commit comments

Comments
 (0)