diff --git a/ranger-authn/pom.xml b/ranger-authn/pom.xml
index 9c0ad8dc59..3d75939692 100644
--- a/ranger-authn/pom.xml
+++ b/ranger-authn/pom.xml
@@ -86,6 +86,12 @@
${nimbus-jose-jwt.version}
+
+ org.springframework.security
+ spring-security-core
+ ${springframework.security.version}
+
+
org.junit.jupiter
diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/authority/JwtAuthority.java b/ranger-authn/src/main/java/org/apache/ranger/authz/authority/JwtAuthority.java
new file mode 100644
index 0000000000..bee9e5a2cf
--- /dev/null
+++ b/ranger-authn/src/main/java/org/apache/ranger/authz/authority/JwtAuthority.java
@@ -0,0 +1,43 @@
+/*
+ * 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.authority;
+
+import java.util.Set;
+import org.springframework.security.core.GrantedAuthority;
+
+public final class JwtAuthority implements GrantedAuthority {
+ private static final long serialVersionUID = 12323L;
+ private final String role;
+ private final Set groups;
+
+ public JwtAuthority(String role, Set groups) {
+ this.role = role;
+ this.groups = groups;
+ }
+
+ public String getAuthority() {
+ return this.role;
+ }
+
+ public Set getGroups() { return this.groups; }
+
+ public String toString() {
+ return this.role;
+ }
+}
\ No newline at end of file
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 17063cedfb..7a9cdfa247 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
@@ -20,6 +20,8 @@
import java.net.URL;
import java.text.ParseException;
+import java.util.Set;
+import java.util.HashSet;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -43,12 +45,14 @@
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
public abstract class RangerJwtAuthHandler implements RangerAuthHandler {
private static final Logger LOG = LoggerFactory.getLogger(RangerJwtAuthHandler.class);
private JWSVerifier verifier = null;
+ protected SignedJWT signedJWT = null;
private String jwksProviderUrl = null;
public static final String TYPE = "ranger-jwt"; // Constant that identifies the authentication mechanism.
public static final String KEY_PROVIDER_URL = "jwks.provider-url"; // JWKS provider URL
@@ -56,7 +60,10 @@ public abstract class RangerJwtAuthHandler implements RangerAuthHandler {
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 JWT_AUTHZ_PREFIX = "Bearer ";
+ public static final String CUSTOM_JWT_CLAIM_GROUP_KEY_PARAM = "custom.jwt.claim.group.key";
+ public static final String CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE_DEFAULT = "knox.groups";
+ public String CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE = null;
protected List audiences = null;
protected JWKSource keySource = null;
@@ -76,6 +83,7 @@ public void initialize(final Properties config) throws Exception {
// optional configurations
String pemPublicKey = config.getProperty(KEY_JWT_PUBLIC_KEY);
+ CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE = config.getProperty(CUSTOM_JWT_CLAIM_GROUP_KEY_PARAM, CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE_DEFAULT);
// setup JWT provider public key if configured
if (StringUtils.isNotBlank(pemPublicKey)) {
@@ -112,30 +120,36 @@ protected AuthenticationToken authenticate(final String jwtAuthHeader, final Str
if (StringUtils.isNotBlank(serializedJWT)) {
try {
- final SignedJWT jwtToken = SignedJWT.parse(serializedJWT);
- boolean valid = validateToken(jwtToken);
+ signedJWT = SignedJWT.parse(serializedJWT);
+ JWTClaimsSet claimsSet = getJWTClaimsSet();
+
+ if(LOG.isDebugEnabled()){
+ LOG.debug("RangerJwtAuthHandler.authenticate(): JWTClaimsSet - {}", claimsSet);
+ }
+
+ boolean valid = validateToken();
if (valid) {
String userName;
if (StringUtils.isNotBlank(doAsUser)) {
userName = doAsUser.trim();
} else {
- userName = jwtToken.getJWTClaimsSet().getSubject();
+ userName = claimsSet.getSubject();
}
if (LOG.isDebugEnabled()) {
LOG.debug("RangerJwtAuthHandler.authenticate(): Issuing AuthenticationToken for user: [{}]", userName);
- LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", jwtToken.getJWTClaimsSet().getSubject(), doAsUser);
+ LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", claimsSet.getSubject(), doAsUser);
}
token = new AuthenticationToken(userName, userName, TYPE);
} else {
- LOG.warn("RangerJwtAuthHandler.authenticate(): Validation failed for JWT token: [{}] ", jwtToken.serialize());
+ LOG.warn("RangerJwtAuthHandler.authenticate(): Validation failed for JWT: [{}] ", signedJWT.serialize());
}
- } catch (ParseException pe) {
- LOG.warn("RangerJwtAuthHandler.authenticate(): Unable to parse the JWT token", pe);
+ } catch (ParseException | RuntimeException exp) {
+ LOG.warn("RangerJwtAuthHandler.authenticate(): Unable to parse the JWT", exp);
}
} else {
- LOG.warn("RangerJwtAuthHandler.authenticate(): JWT token not found.");
+ LOG.warn("RangerJwtAuthHandler.authenticate(): JWT not found");
}
}
@@ -145,6 +159,31 @@ protected AuthenticationToken authenticate(final String jwtAuthHeader, final Str
return token;
}
+
+ protected JWTClaimsSet getJWTClaimsSet() throws ParseException {
+ return signedJWT.getJWTClaimsSet();
+ }
+
+ public Set getGroupsFromClaimSet() {
+ List groupsClaim = null;
+ try {
+ groupsClaim = (List) getJWTClaimsSet().getClaim(CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE);
+ } catch (ParseException e) {
+ LOG.error("Unable to parse JWT claim set", e);
+ }
+
+ if (groupsClaim == null) {
+ LOG.warn("No group claim found!");
+ return new HashSet<>();
+ }
+
+ Set groups = new HashSet<>(groupsClaim);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Groups present in Claim [{}]: {}", CUSTOM_JWT_CLAIM_GROUP_KEY_VALUE, groups);
+ }
+ return groups;
+ }
+
protected String getJWT(final String jwtAuthHeader, final String jwtCookie) {
String serializedJWT = null;
@@ -171,19 +210,18 @@ protected String getJWT(final String jwtAuthHeader, final String jwtCookie) {
* implementation through submethods used within but also allows for the
* override of the entire token validation algorithm.
*
- * @param jwtToken the token to validate
* @return true if valid
*/
- protected boolean validateToken(final SignedJWT jwtToken) {
- boolean expValid = validateExpiration(jwtToken);
+ protected boolean validateToken() throws ParseException {
+ boolean expValid = validateExpiration();
boolean sigValid = false;
boolean audValid = false;
if (expValid) {
- sigValid = validateSignature(jwtToken);
+ sigValid = validateSignature();
if (sigValid) {
- audValid = validateAudiences(jwtToken);
+ audValid = validateAudiences();
}
}
@@ -195,41 +233,40 @@ protected boolean validateToken(final SignedJWT jwtToken) {
}
/**
- * Verify the signature of the JWT token in this method. This method depends on
+ * Verify the signature of the JWT in this method. This method depends on
* the public key that was established during init based upon the provisioned
* public key. Override this method in subclasses in order to customize the
* signature verification behavior.
*
- * @param jwtToken the token that contains the signature to be validated
* @return valid true if signature verifies successfully; false otherwise
*/
- protected boolean validateSignature(final SignedJWT jwtToken) {
+ protected boolean validateSignature() {
boolean valid = false;
- if (JWSObject.State.SIGNED == jwtToken.getState()) {
+ if (JWSObject.State.SIGNED == signedJWT.getState()) {
if (LOG.isDebugEnabled()) {
- LOG.debug("JWT token is in a SIGNED state");
+ LOG.debug("JWT is in a SIGNED state");
}
- if (jwtToken.getSignature() != null) {
+ if (signedJWT.getSignature() != null) {
try {
if (StringUtils.isNotBlank(jwksProviderUrl)) {
- JWSKeySelector keySelector = new JWSVerificationKeySelector<>(jwtToken.getHeader().getAlgorithm(), keySource);
+ JWSKeySelector keySelector = new JWSVerificationKeySelector<>(signedJWT.getHeader().getAlgorithm(), keySource);
// Create a JWT processor for the access tokens
ConfigurableJWTProcessor jwtProcessor = getJwtProcessor(keySelector);
// Process the token
- jwtProcessor.process(jwtToken, null);
+ jwtProcessor.process(signedJWT, null);
valid = true;
if (LOG.isDebugEnabled()) {
- LOG.debug("JWT token has been successfully verified.");
+ LOG.debug("JWT has been successfully verified.");
}
} else if (verifier != null) {
- if (jwtToken.verify(verifier)) {
+ if (signedJWT.verify(verifier)) {
valid = true;
if (LOG.isDebugEnabled()) {
- LOG.debug("JWT token has been successfully verified.");
+ LOG.debug("JWT has been successfully verified.");
}
} else {
LOG.warn("JWT signature verification failed.");
@@ -257,61 +294,51 @@ protected boolean validateSignature(final SignedJWT jwtToken) {
* token claims list for audience. Override this method in subclasses in order
* to customize the audience validation behavior.
*
- * @param jwtToken the JWT token where the allowed audiences will be found
* @return true if an expected audience is present, otherwise false
*/
- protected boolean validateAudiences(final SignedJWT jwtToken) {
+ protected boolean validateAudiences() throws ParseException {
boolean valid = false;
- try {
- List tokenAudienceList = jwtToken.getJWTClaimsSet().getAudience();
- // if there were no expected audiences configured then just
- // consider any audience acceptable
- if (audiences == null) {
- valid = true;
- } else {
- // if any of the configured audiences is found then consider it
- // acceptable
- for (String aud : tokenAudienceList) {
- if (audiences.contains(aud)) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("JWT token audience has been successfully validated.");
- }
- valid = true;
- break;
+ JWTClaimsSet claimsSet = getJWTClaimsSet();
+ List tokenAudienceList = claimsSet.getAudience();
+ // if there were no expected audiences configured then just consider any audience acceptable
+ if (audiences == null) {
+ valid = true;
+ } else {
+ // if any of the configured audiences is found then consider it acceptable
+ for (String aud : tokenAudienceList) {
+ if (audiences.contains(aud)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("JWT audience has been successfully validated.");
}
- }
- if (!valid) {
- LOG.warn("JWT audience validation failed.");
+ valid = true;
+ break;
}
}
- } catch (ParseException pe) {
- LOG.warn("Unable to parse the JWT token.", pe);
+ }
+
+ if (!valid) {
+ LOG.warn("JWT audience validation failed.");
}
return valid;
}
/**
- * Validate that the expiration time of the JWT token has not been violated. If
- * it has then throw an AuthenticationException. Override this method in
+ * Validate that the expiration time of the JWT has not been violated. If
+ * it has, then throw an AuthenticationException. Override this method in
* subclasses in order to customize the expiration validation behavior.
*
- * @param jwtToken the token that contains the expiration date to validate
* @return valid true if the token has not expired; false otherwise
*/
- protected boolean validateExpiration(final SignedJWT jwtToken) {
+ protected boolean validateExpiration() throws ParseException {
boolean valid = false;
- try {
- Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
- if (expires == null || new Date().before(expires)) {
- valid = true;
- if (LOG.isDebugEnabled()) {
- LOG.debug("JWT token expiration date has been successfully validated.");
- }
- } else {
- LOG.warn("JWT token provided is expired.");
+ Date expires = getJWTClaimsSet().getExpirationTime();
+ if (expires == null || new Date().before(expires)) {
+ valid = true;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("JWT expiration date has been successfully validated.");
}
- } catch (ParseException pe) {
- LOG.warn("Failed to validate JWT expiry.", pe);
+ } else {
+ LOG.warn("JWT provided has expired.");
}
return valid;
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 f14adaaa8d..27acdd7dd7 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
@@ -19,6 +19,7 @@
package org.apache.ranger.security.web.filter;
import java.io.IOException;
+import java.util.Set;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
@@ -33,6 +34,7 @@
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
+import org.apache.ranger.authz.authority.JwtAuthority;
import org.apache.ranger.authz.handler.RangerAuth;
import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
import org.apache.ranger.authz.handler.jwt.RangerJwtAuthHandler;
@@ -42,7 +44,6 @@
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
@@ -101,7 +102,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
RangerAuth rangerAuth = authenticate(httpServletRequest);
if (rangerAuth != null) {
- final List grantedAuths = Arrays.asList(new SimpleGrantedAuthority(DEFAULT_RANGER_ROLE));
+ final Set groups = getGroupsFromClaimSet();
+ final List grantedAuths = Arrays.asList(new JwtAuthority(DEFAULT_RANGER_ROLE, groups));
final UserDetails principal = new User(rangerAuth.getUserName(), "", grantedAuths);
final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
final WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpServletRequest);