/*
* 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.security.web.filter;
import com.google.inject.Inject;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.SignedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
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;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.apache.ranger.biz.UserMgr;
import org.apache.ranger.common.PropertiesUtil;
import org.apache.ranger.common.UserSessionBase;
import org.apache.ranger.security.context.RangerContextHolder;
import org.apache.ranger.security.context.RangerSecurityContext;
import org.apache.ranger.security.handler.RangerAuthenticationProvider;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class RangerSSOAuthenticationFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(RangerSSOAuthenticationFilter.class);
public static final String BROWSER_USERAGENT = "ranger.sso.browser.useragent";
public static final String JWT_AUTH_PROVIDER_URL = "ranger.sso.providerurl";
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_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";
public static final String LOCAL_LOGIN_URL = "locallogin";
private SSOAuthenticationProperties jwtProperties;
private String originalUrlQueryParam = "originalUrl";
private String authenticationProviderUrl = null;
private RSAPublicKey publicKey = null;
private String cookieName = "hadoop-jwt";
private boolean ssoEnabled = false;
@Autowired
UserMgr userMgr;
@Inject
public RangerSSOAuthenticationFilter(){
jwtProperties = getJwtProperties();
loadJwtProperties();
}
public RangerSSOAuthenticationFilter(
SSOAuthenticationProperties jwtProperties){
this.jwtProperties = jwtProperties;
loadJwtProperties();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/*
* doFilter of RangerSSOAuthenticationFilter is the first in the filter list so in this it check for the request
* if the request is from browser, doesn't contain local login and sso is enabled then it process the request against knox sso
* else if it's ssoenable and the request is with local login string then it show's the appropriate msg
* else if ssoenable is false then it contiunes with further filters as it was before sso
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
if (httpRequest.getRequestedSessionId() != null && !httpRequest.isRequestedSessionIdValid()){
synchronized(httpRequest.getServletContext()){
if(httpRequest.getServletContext().getAttribute(httpRequest.getRequestedSessionId()) != null && "locallogin".equals(httpRequest.getServletContext().getAttribute(httpRequest.getRequestedSessionId()).toString())){
ssoEnabled = false;
httpRequest.getSession().setAttribute("locallogin","true");
httpRequest.getServletContext().removeAttribute(httpRequest.getRequestedSessionId());
}
}
}
RangerSecurityContext context = RangerContextHolder.getSecurityContext();
UserSessionBase session = context != null ? context.getUserSession() : null;
ssoEnabled = session != null ? session.isSSOEnabled() : PropertiesUtil.getBooleanProperty("ranger.sso.enabled", false);
String userAgent = httpRequest.getHeader("User-Agent");
if(httpRequest.getSession() != null){
if(httpRequest.getSession().getAttribute("locallogin") != null){
ssoEnabled = false;
servletRequest.setAttribute("ssoEnabled", false);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
}
//If sso is enable and request is not for local login and is from browser then it will go inside and try for knox sso authentication
if (ssoEnabled && !httpRequest.getRequestURI().contains(LOCAL_LOGIN_URL) && isWebUserAgent(userAgent)) {
//if jwt properties are loaded and is current not authenticated then it will go for sso authentication
//Note : Need to remove !isAuthenticated() after knoxsso solve the bug from cross-origin script
if (jwtProperties != null && !isAuthenticated()) {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
String serializedJWT = getJWTFromCookie(httpRequest);
// if we get the hadoop-jwt token from the cookies then will process it further
if (serializedJWT != null) {
SignedJWT jwtToken = null;
try {
jwtToken = SignedJWT.parse(serializedJWT);
boolean valid = validateToken(jwtToken);
//if the public key provide is correct and also token is not expired the process token
if (valid) {
String userName = jwtToken.getJWTClaimsSet().getSubject();
LOG.info("SSO login user : "+userName);
String rangerLdapDefaultRole = PropertiesUtil.getProperty("ranger.ldap.default.role", "ROLE_USER");
//if we get the userName from the token then log into ranger using the same user
if (userName != null && !userName.trim().isEmpty()) {
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority(rangerLdapDefaultRole));
final UserDetails principal = new User(userName, "",grantedAuths);
final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
((AbstractAuthenticationToken) finalAuthentication).setDetails(webDetails);
RangerAuthenticationProvider authenticationProvider = new RangerAuthenticationProvider();
authenticationProvider.setSsoEnabled(ssoEnabled);
Authentication authentication = authenticationProvider.authenticate(finalAuthentication);
authentication = getGrantedAuthority(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest,httpServletResponse);
}
// if the token is not valid then redirect to knox sso
else {
String ssourl = constructLoginURL(httpRequest);
if(LOG.isDebugEnabled())
LOG.debug("SSO URL = " + ssourl);
httpServletResponse.sendRedirect(ssourl);
}
} catch (ParseException e) {
LOG.warn("Unable to parse the JWT token", e);
}
}
// if the jwt token is not available then redirect it to knox sso
else {
String ssourl = constructLoginURL(httpRequest);
if(LOG.isDebugEnabled())
LOG.debug("SSO URL = " + ssourl);
httpServletResponse.sendRedirect(ssourl);
}
}
//if property is not loaded or is already authenticated then proceed further with next filter
else {
filterChain.doFilter(servletRequest, servletResponse);
}
} else if(ssoEnabled && ((HttpServletRequest) servletRequest).getRequestURI().contains(LOCAL_LOGIN_URL) && isWebUserAgent(userAgent) && isAuthenticated()){
//If already there's an active session with sso and user want's to switch to local login(i.e without sso) then it won't be navigated to local login
// In this scenario the user as to use separate browser
String url = ((HttpServletRequest) servletRequest).getRequestURI().replace(LOCAL_LOGIN_URL+"/", "");
url = url.replace(LOCAL_LOGIN_URL, "");
LOG.warn("There is an active session and if you want local login to ranger, try this on a separate browser");
((HttpServletResponse)servletResponse).sendRedirect(url);
}
//if sso is not enable or the request is not from browser then proceed further with next filter
else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
private Authentication getGrantedAuthority(Authentication authentication) {
UsernamePasswordAuthenticationToken result=null;
if(authentication!=null && authentication.isAuthenticated()){
final List<GrantedAuthority> grantedAuths=getAuthorities(authentication.getName().toString());
final UserDetails userDetails = new User(authentication.getName().toString(), authentication.getCredentials().toString(),grantedAuths);
result = new UsernamePasswordAuthenticationToken(userDetails,authentication.getCredentials(),grantedAuths);
result.setDetails(authentication.getDetails());
return result;
}
return authentication;
}
private List<GrantedAuthority> getAuthorities(String username) {
Collection<String> roleList=userMgr.getRolesByLoginId(username);
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
for(String role:roleList){
grantedAuths.add(new SimpleGrantedAuthority(role));
}
return grantedAuths;
}
private boolean isWebUserAgent(String userAgent) {
boolean isWeb = false;
if (jwtProperties != null) {
String userAgentList[] = jwtProperties.getUserAgentList();
if(userAgentList != null && userAgentList.length > 0){
for(String ua : userAgentList){
if(userAgent.toLowerCase().startsWith(ua.toLowerCase())){
isWeb = true;
break;
}
}
}
}
return isWeb;
}
/**
* @return the ssoEnabled
*/
public boolean isSsoEnabled() {
return ssoEnabled;
}
/**
* @param ssoEnabled the ssoEnabled to set
*/
public void setSsoEnabled(boolean ssoEnabled) {
this.ssoEnabled = ssoEnabled;
}
private void loadJwtProperties() {
if (jwtProperties != null) {
authenticationProviderUrl = jwtProperties.getAuthenticationProviderUrl();
publicKey = jwtProperties.getPublicKey();
cookieName = jwtProperties.getCookieName();
originalUrlQueryParam = jwtProperties.getOriginalUrlQueryParam();
}
}
/**
* Do not try to validate JWT if user already authenticated via other
* provider
*
* @return true, if JWT validation required
*/
private boolean isAuthenticated() {
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
return !(!(existingAuth != null && existingAuth.isAuthenticated()) || existingAuth instanceof SSOAuthentication);
}
/**
* Encapsulate the acquisition of the JWT token from HTTP cookies within the
* request.
*
* @param req
* servlet request to get the JWT token from
* @return serialized JWT token
*/
protected String getJWTFromCookie(HttpServletRequest req) {
String serializedJWT = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName != null && cookieName.equals(cookie.getName())) {
if(LOG.isDebugEnabled())
LOG.debug(cookieName + " cookie has been found and is being processed");
serializedJWT = cookie.getValue();
break;
}
}
}
return serializedJWT;
}
/**
* Create the URL to be used for authentication of the user in the absence
* of a JWT token within the incoming request.
*
* @param request
* for getting the original request URL
* @return url to use as login url for redirect
*/
protected String constructLoginURL(HttpServletRequest request) {
String delimiter = "?";
if (authenticationProviderUrl.contains("?")) {
delimiter = "&";
}
String loginURL = authenticationProviderUrl + delimiter + originalUrlQueryParam + "=" + request.getRequestURL().append(getOriginalQueryString(request));
return loginURL;
}
private String getOriginalQueryString(HttpServletRequest request) {
String originalQueryString = request.getQueryString();
return (originalQueryString == null) ? "" : "?" + originalQueryString;
}
/**
* This method provides a single method for validating the JWT for use in
* request processing. It provides for the override of specific aspects of
* this 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(SignedJWT jwtToken) {
boolean sigValid = validateSignature(jwtToken);
if (!sigValid) {
LOG.warn("Signature of JWT token could not be verified. Please check the public key");
}
boolean expValid = validateExpiration(jwtToken);
if (!expValid) {
LOG.warn("Expiration time validation of JWT token failed.");
}
return sigValid && expValid;
}
/**
* Verify the signature of the JWT token 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(SignedJWT jwtToken) {
boolean valid = false;
if (JWSObject.State.SIGNED == jwtToken.getState()) {
if(LOG.isDebugEnabled())
LOG.debug("SSO token is in a SIGNED state");
if (jwtToken.getSignature() != null) {
if(LOG.isDebugEnabled())
LOG.debug("SSO token signature is not null");
try {
JWSVerifier verifier = new RSASSAVerifier(publicKey);
if (jwtToken.verify(verifier)) {
valid = true;
if(LOG.isDebugEnabled())
LOG.debug("SSO token has been successfully verified");
} else {
LOG.warn("SSO signature verification failed.Please check the public key");
}
} catch (JOSEException je) {
LOG.warn("Error while validating signature", je);
}catch(Exception e){
LOG.warn("Error while validating signature", e);
}
}
}
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
* 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(SignedJWT jwtToken) {
boolean valid = false;
try {
Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
if (expires == null || new Date().before(expires)) {
if(LOG.isDebugEnabled())
LOG.debug("SSO token expiration date has been " + "successfully validated");
valid = true;
} else {
LOG.warn("SSO expiration date validation failed.");
}
} catch (ParseException pe) {
LOG.warn("SSO expiration date validation failed.", pe);
}
return valid;
}
@Override
public void destroy() {
}
public SSOAuthenticationProperties getJwtProperties() {
String providerUrl = PropertiesUtil.getProperty(JWT_AUTH_PROVIDER_URL);
if (providerUrl != null && PropertiesUtil.getBooleanProperty("ranger.sso.enabled", false)) {
SSOAuthenticationProperties jwtProperties = new SSOAuthenticationProperties();
String publicKeyPath = PropertiesUtil.getProperty(JWT_PUBLIC_KEY);
if (publicKeyPath == null) {
LOG.error("Public key pem not specified for SSO auth provider {}. SSO auth will be disabled.",providerUrl);
return null;
}
jwtProperties.setAuthenticationProviderUrl(providerUrl);
jwtProperties.setCookieName(PropertiesUtil.getProperty(JWT_COOKIE_NAME, JWT_COOKIE_NAME_DEFAULT));
jwtProperties.setOriginalUrlQueryParam(PropertiesUtil.getProperty(JWT_ORIGINAL_URL_QUERY_PARAM, JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT));
String userAgent = PropertiesUtil.getProperty(BROWSER_USERAGENT);
if(userAgent != null && !userAgent.isEmpty()){
jwtProperties.setUserAgentList(userAgent.split(","));
}
try {
RSAPublicKey publicKey = parseRSAPublicKey(publicKeyPath);
jwtProperties.setPublicKey(publicKey);
} catch (IOException e) {
LOG.error("Unable to read public certificate file. JWT auth will be disabled.",e);
} catch (CertificateException e) {
LOG.error("Unable to parse public certificate file. JWT auth will be disabled.",e);
} catch (ServletException e) {
LOG.error("ServletException while processing the properties",e);
}
return jwtProperties;
} else {
return null;
}
}
/*
* public static RSAPublicKey getPublicKeyFromFile(String filePath) throws
* IOException, CertificateException {
* FileUtils.readFileToString(new File(filePath));
* getPublicKeyFromString(pemString); }
*/
public static RSAPublicKey parseRSAPublicKey(String pem)
throws CertificateException, UnsupportedEncodingException,
ServletException {
String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n";
String PEM_FOOTER = "\n-----END CERTIFICATE-----";
String fullPem = PEM_HEADER + pem + PEM_FOOTER;
PublicKey key = null;
try {
CertificateFactory fact = CertificateFactory.getInstance("X.509");
ByteArrayInputStream is = new ByteArrayInputStream(fullPem.getBytes("UTF8"));
X509Certificate cer = (X509Certificate) fact.generateCertificate(is);
key = cer.getPublicKey();
} catch (CertificateException ce) {
String message = null;
if (pem.startsWith(PEM_HEADER)) {
message = "CertificateException - be sure not to include PEM header " + "and footer in the PEM configuration element.";
} else {
message = "CertificateException - PEM may be corrupt";
}
throw new ServletException(message, ce);
} catch (UnsupportedEncodingException uee) {
throw new ServletException(uee);
}
return (RSAPublicKey) key;
}
}