Skip to content

Commit e3cce5b

Browse files
author
Abhishek Kumar
committed
RANGER-4955: Add support to retrieve group information from JWT
1 parent f06d0e7 commit e3cce5b

File tree

4 files changed

+142
-64
lines changed

4 files changed

+142
-64
lines changed

ranger-authn/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@
8686
<version>${nimbus-jose-jwt.version}</version>
8787
</dependency>
8888

89+
<dependency>
90+
<groupId>org.springframework.security</groupId>
91+
<artifactId>spring-security-core</artifactId>
92+
<version>${springframework.security.version}</version>
93+
</dependency>
94+
8995
<!-- Test -->
9096
<dependency>
9197
<groupId>org.junit.jupiter</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.authority;
20+
21+
import java.util.Set;
22+
import org.springframework.security.core.GrantedAuthority;
23+
24+
public final class JwtAuthority implements GrantedAuthority {
25+
private static final long serialVersionUID = 12323L;
26+
private final String role;
27+
private final Set<String> groups;
28+
29+
public JwtAuthority(String role, Set<String> groups) {
30+
this.role = role;
31+
this.groups = groups;
32+
}
33+
34+
public String getAuthority() {
35+
return this.role;
36+
}
37+
38+
public Set<String> getGroups() { return this.groups; }
39+
40+
public String toString() {
41+
return this.role;
42+
}
43+
}

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

+89-62
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import java.net.URL;
2222
import java.text.ParseException;
23+
import java.util.Set;
24+
import java.util.HashSet;
2325
import java.util.Arrays;
2426
import java.util.Date;
2527
import java.util.List;
@@ -43,20 +45,25 @@
4345
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
4446
import com.nimbusds.jose.proc.SecurityContext;
4547
import com.nimbusds.jwt.SignedJWT;
48+
import com.nimbusds.jwt.JWTClaimsSet;
4649
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
4750

4851
public abstract class RangerJwtAuthHandler implements RangerAuthHandler {
4952
private static final Logger LOG = LoggerFactory.getLogger(RangerJwtAuthHandler.class);
5053

5154
private JWSVerifier verifier = null;
55+
protected SignedJWT signedJWT = null;
5256
private String jwksProviderUrl = null;
5357
public static final String TYPE = "ranger-jwt"; // Constant that identifies the authentication mechanism.
5458
public static final String KEY_PROVIDER_URL = "jwks.provider-url"; // JWKS provider URL
5559
public static final String KEY_JWT_PUBLIC_KEY = "jwt.public-key"; // JWT token provider public key
5660
public static final String KEY_JWT_COOKIE_NAME = "jwt.cookie-name"; // JWT cookie name
5761
public static final String KEY_JWT_AUDIENCES = "jwt.audiences";
5862
public static final String JWT_AUTHZ_PREFIX = "Bearer ";
63+
public static final String CUSTOM_JWT_CLAIM_GROUP_KEY_PARAM = "custom.jwt.claim.group.key";
64+
public static final String CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE_DEFAULT = "knox.groups";
5965

66+
public String CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE = null;
6067
protected List<String> audiences = null;
6168
protected JWKSource<SecurityContext> keySource = null;
6269

@@ -76,6 +83,7 @@ public void initialize(final Properties config) throws Exception {
7683

7784
// optional configurations
7885
String pemPublicKey = config.getProperty(KEY_JWT_PUBLIC_KEY);
86+
CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE = config.getProperty(CUSTOM_JWT_CLAIM_GROUP_KEY_PARAM, CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE_DEFAULT);
7987

8088
// setup JWT provider public key if configured
8189
if (StringUtils.isNotBlank(pemPublicKey)) {
@@ -112,30 +120,36 @@ protected AuthenticationToken authenticate(final String jwtAuthHeader, final Str
112120

113121
if (StringUtils.isNotBlank(serializedJWT)) {
114122
try {
115-
final SignedJWT jwtToken = SignedJWT.parse(serializedJWT);
116-
boolean valid = validateToken(jwtToken);
123+
signedJWT = SignedJWT.parse(serializedJWT);
124+
JWTClaimsSet claimsSet = getJWTClaimsSet();
125+
126+
if(LOG.isDebugEnabled()){
127+
LOG.debug("RangerJwtAuthHandler.authenticate(): JWTClaimsSet - {}", claimsSet);
128+
}
129+
130+
boolean valid = validateToken();
117131
if (valid) {
118132
String userName;
119133

120134
if (StringUtils.isNotBlank(doAsUser)) {
121135
userName = doAsUser.trim();
122136
} else {
123-
userName = jwtToken.getJWTClaimsSet().getSubject();
137+
userName = claimsSet.getSubject();
124138
}
125139

126140
if (LOG.isDebugEnabled()) {
127141
LOG.debug("RangerJwtAuthHandler.authenticate(): Issuing AuthenticationToken for user: [{}]", userName);
128-
LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", jwtToken.getJWTClaimsSet().getSubject(), doAsUser);
142+
LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", claimsSet.getSubject(), doAsUser);
129143
}
130144
token = new AuthenticationToken(userName, userName, TYPE);
131145
} else {
132-
LOG.warn("RangerJwtAuthHandler.authenticate(): Validation failed for JWT token: [{}] ", jwtToken.serialize());
146+
LOG.warn("RangerJwtAuthHandler.authenticate(): Validation failed for JWT: [{}] ", signedJWT.serialize());
133147
}
134-
} catch (ParseException pe) {
135-
LOG.warn("RangerJwtAuthHandler.authenticate(): Unable to parse the JWT token", pe);
148+
} catch (ParseException | RuntimeException exp) {
149+
LOG.warn("RangerJwtAuthHandler.authenticate(): Unable to parse the JWT", exp);
136150
}
137151
} else {
138-
LOG.warn("RangerJwtAuthHandler.authenticate(): JWT token not found.");
152+
LOG.warn("RangerJwtAuthHandler.authenticate(): JWT not found");
139153
}
140154
}
141155

@@ -145,6 +159,31 @@ protected AuthenticationToken authenticate(final String jwtAuthHeader, final Str
145159

146160
return token;
147161
}
162+
163+
protected JWTClaimsSet getJWTClaimsSet() throws ParseException {
164+
return signedJWT.getJWTClaimsSet();
165+
}
166+
167+
public Set<String> getGroupsFromClaimSet() {
168+
List<String> groupsClaim = null;
169+
try {
170+
groupsClaim = (List<String>) getJWTClaimsSet().getClaim(CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE);
171+
} catch (ParseException e) {
172+
LOG.error("Unable to parse JWT claim set", e);
173+
}
174+
175+
if (groupsClaim == null) {
176+
LOG.warn("No group claim found!");
177+
return new HashSet<>();
178+
}
179+
180+
Set<String> groups = new HashSet<>(groupsClaim);
181+
if (LOG.isDebugEnabled()) {
182+
LOG.debug("Groups present in Claim [{}]: {}", CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE, groups);
183+
}
184+
return groups;
185+
}
186+
148187

149188
protected String getJWT(final String jwtAuthHeader, final String jwtCookie) {
150189
String serializedJWT = null;
@@ -171,19 +210,18 @@ protected String getJWT(final String jwtAuthHeader, final String jwtCookie) {
171210
* implementation through submethods used within but also allows for the
172211
* override of the entire token validation algorithm.
173212
*
174-
* @param jwtToken the token to validate
175213
* @return true if valid
176214
*/
177-
protected boolean validateToken(final SignedJWT jwtToken) {
178-
boolean expValid = validateExpiration(jwtToken);
215+
protected boolean validateToken() throws ParseException {
216+
boolean expValid = validateExpiration();
179217
boolean sigValid = false;
180218
boolean audValid = false;
181219

182220
if (expValid) {
183-
sigValid = validateSignature(jwtToken);
221+
sigValid = validateSignature();
184222

185223
if (sigValid) {
186-
audValid = validateAudiences(jwtToken);
224+
audValid = validateAudiences();
187225
}
188226
}
189227

@@ -195,41 +233,40 @@ protected boolean validateToken(final SignedJWT jwtToken) {
195233
}
196234

197235
/**
198-
* Verify the signature of the JWT token in this method. This method depends on
236+
* Verify the signature of the JWT in this method. This method depends on
199237
* the public key that was established during init based upon the provisioned
200238
* public key. Override this method in subclasses in order to customize the
201239
* signature verification behavior.
202240
*
203-
* @param jwtToken the token that contains the signature to be validated
204241
* @return valid true if signature verifies successfully; false otherwise
205242
*/
206-
protected boolean validateSignature(final SignedJWT jwtToken) {
243+
protected boolean validateSignature() {
207244
boolean valid = false;
208245

209-
if (JWSObject.State.SIGNED == jwtToken.getState()) {
246+
if (JWSObject.State.SIGNED == signedJWT.getState()) {
210247
if (LOG.isDebugEnabled()) {
211-
LOG.debug("JWT token is in a SIGNED state");
248+
LOG.debug("JWT is in a SIGNED state");
212249
}
213250

214-
if (jwtToken.getSignature() != null) {
251+
if (signedJWT.getSignature() != null) {
215252
try {
216253
if (StringUtils.isNotBlank(jwksProviderUrl)) {
217-
JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<>(jwtToken.getHeader().getAlgorithm(), keySource);
254+
JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<>(signedJWT.getHeader().getAlgorithm(), keySource);
218255

219256
// Create a JWT processor for the access tokens
220257
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = getJwtProcessor(keySelector);
221258

222259
// Process the token
223-
jwtProcessor.process(jwtToken, null);
260+
jwtProcessor.process(signedJWT, null);
224261
valid = true;
225262
if (LOG.isDebugEnabled()) {
226-
LOG.debug("JWT token has been successfully verified.");
263+
LOG.debug("JWT has been successfully verified.");
227264
}
228265
} else if (verifier != null) {
229-
if (jwtToken.verify(verifier)) {
266+
if (signedJWT.verify(verifier)) {
230267
valid = true;
231268
if (LOG.isDebugEnabled()) {
232-
LOG.debug("JWT token has been successfully verified.");
269+
LOG.debug("JWT has been successfully verified.");
233270
}
234271
} else {
235272
LOG.warn("JWT signature verification failed.");
@@ -257,61 +294,51 @@ protected boolean validateSignature(final SignedJWT jwtToken) {
257294
* token claims list for audience. Override this method in subclasses in order
258295
* to customize the audience validation behavior.
259296
*
260-
* @param jwtToken the JWT token where the allowed audiences will be found
261297
* @return true if an expected audience is present, otherwise false
262298
*/
263-
protected boolean validateAudiences(final SignedJWT jwtToken) {
299+
protected boolean validateAudiences() throws ParseException {
264300
boolean valid = false;
265-
try {
266-
List<String> tokenAudienceList = jwtToken.getJWTClaimsSet().getAudience();
267-
// if there were no expected audiences configured then just
268-
// consider any audience acceptable
269-
if (audiences == null) {
270-
valid = true;
271-
} else {
272-
// if any of the configured audiences is found then consider it
273-
// acceptable
274-
for (String aud : tokenAudienceList) {
275-
if (audiences.contains(aud)) {
276-
if (LOG.isDebugEnabled()) {
277-
LOG.debug("JWT token audience has been successfully validated.");
278-
}
279-
valid = true;
280-
break;
301+
JWTClaimsSet claimsSet = getJWTClaimsSet();
302+
List<String> tokenAudienceList = claimsSet.getAudience();
303+
// if there were no expected audiences configured then just consider any audience acceptable
304+
if (audiences == null) {
305+
valid = true;
306+
} else {
307+
// if any of the configured audiences is found then consider it acceptable
308+
for (String aud : tokenAudienceList) {
309+
if (audiences.contains(aud)) {
310+
if (LOG.isDebugEnabled()) {
311+
LOG.debug("JWT audience has been successfully validated.");
281312
}
282-
}
283-
if (!valid) {
284-
LOG.warn("JWT audience validation failed.");
313+
valid = true;
314+
break;
285315
}
286316
}
287-
} catch (ParseException pe) {
288-
LOG.warn("Unable to parse the JWT token.", pe);
317+
}
318+
319+
if (!valid) {
320+
LOG.warn("JWT audience validation failed.");
289321
}
290322
return valid;
291323
}
292324

293325
/**
294-
* Validate that the expiration time of the JWT token has not been violated. If
295-
* it has then throw an AuthenticationException. Override this method in
326+
* Validate that the expiration time of the JWT has not been violated. If
327+
* it has, then throw an AuthenticationException. Override this method in
296328
* subclasses in order to customize the expiration validation behavior.
297329
*
298-
* @param jwtToken the token that contains the expiration date to validate
299330
* @return valid true if the token has not expired; false otherwise
300331
*/
301-
protected boolean validateExpiration(final SignedJWT jwtToken) {
332+
protected boolean validateExpiration() throws ParseException {
302333
boolean valid = false;
303-
try {
304-
Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
305-
if (expires == null || new Date().before(expires)) {
306-
valid = true;
307-
if (LOG.isDebugEnabled()) {
308-
LOG.debug("JWT token expiration date has been successfully validated.");
309-
}
310-
} else {
311-
LOG.warn("JWT token provided is expired.");
334+
Date expires = getJWTClaimsSet().getExpirationTime();
335+
if (expires == null || new Date().before(expires)) {
336+
valid = true;
337+
if (LOG.isDebugEnabled()) {
338+
LOG.debug("JWT expiration date has been successfully validated.");
312339
}
313-
} catch (ParseException pe) {
314-
LOG.warn("Failed to validate JWT expiry.", pe);
340+
} else {
341+
LOG.warn("JWT provided has expired.");
315342
}
316343

317344
return valid;

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.ranger.security.web.filter;
2020

2121
import java.io.IOException;
22+
import java.util.Set;
2223
import java.util.Arrays;
2324
import java.util.List;
2425
import java.util.Properties;
@@ -33,6 +34,7 @@
3334
import javax.servlet.http.HttpServletRequest;
3435

3536
import org.apache.log4j.Logger;
37+
import org.apache.ranger.authz.authority.JwtAuthority;
3638
import org.apache.ranger.authz.handler.RangerAuth;
3739
import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
3840
import org.apache.ranger.authz.handler.jwt.RangerJwtAuthHandler;
@@ -42,7 +44,6 @@
4244
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
4345
import org.springframework.security.core.Authentication;
4446
import org.springframework.security.core.GrantedAuthority;
45-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
4647
import org.springframework.security.core.context.SecurityContextHolder;
4748
import org.springframework.security.core.userdetails.User;
4849
import org.springframework.security.core.userdetails.UserDetails;
@@ -101,7 +102,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
101102
RangerAuth rangerAuth = authenticate(httpServletRequest);
102103

103104
if (rangerAuth != null) {
104-
final List<GrantedAuthority> grantedAuths = Arrays.asList(new SimpleGrantedAuthority(DEFAULT_RANGER_ROLE));
105+
final Set<String> groups = getGroupsFromClaimSet();
106+
final List<GrantedAuthority> grantedAuths = Arrays.asList(new JwtAuthority(DEFAULT_RANGER_ROLE, groups));
105107
final UserDetails principal = new User(rangerAuth.getUserName(), "", grantedAuths);
106108
final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
107109
final WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpServletRequest);

0 commit comments

Comments
 (0)