blob: e58063a005f4a2725b624c2c337e53e349999e83 [file] [log] [blame]
/**
* 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.metron.rest.config;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.SignedJWT;
import org.apache.metron.rest.security.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.support.LdapNameBuilder;
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.SecurityContext;
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.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_PREFIX;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
/**
* This class is a Servlet Filter that authenticates a Knox SSO token. The token is stored in a cookie and is
* verified against a public Knox key. The token expiration and begin time are also validated. Upon successful validation,
* a Spring Authentication object is built from the user name and user groups queried from LDAP. Currently, user groups are
* mapped directly to Spring roles and prepended with "ROLE_".
*/
public class KnoxSSOAuthenticationFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(KnoxSSOAuthenticationFilter.class);
private String userSearchBase;
private Path knoxKeyFile;
private String knoxKeyString;
private String knoxCookie;
private LdapTemplate ldapTemplate;
public KnoxSSOAuthenticationFilter(String userSearchBase,
Path knoxKeyFile,
String knoxKeyString,
String knoxCookie,
LdapTemplate ldapTemplate) {
this.userSearchBase = userSearchBase;
this.knoxKeyFile = knoxKeyFile;
this.knoxKeyString = knoxKeyString;
this.knoxCookie = knoxCookie;
if (ldapTemplate == null) {
throw new IllegalStateException("KnoxSSO requires LDAP. You must add 'ldap' to the active profiles.");
}
this.ldapTemplate = ldapTemplate;
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
/**
* Extracts the Knox token from the configured cookie. If basic authentication headers are present, SSO authentication
* is skipped.
* @param request ServletRequest
* @param response ServletResponse
* @param chain FilterChain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// If a basic authentication header is present, use that to authenticate and skip SSO
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Basic")) {
String serializedJWT = getJWTFromCookie(httpRequest);
if (serializedJWT != null) {
SignedJWT jwtToken;
try {
jwtToken = parseJWT(serializedJWT);
String userName = jwtToken.getJWTClaimsSet().getSubject();
LOG.info("SSO login user : {} ", userName);
if (isValid(jwtToken, userName)) {
Authentication authentication = getAuthentication(userName, httpRequest);
getSecurityContext().setAuthentication(authentication);
}
} catch (ParseException e) {
LOG.warn("Unable to parse the JWT token", e);
}
}
}
chain.doFilter(request, response);
}
// exposed for testing
protected SecurityContext getSecurityContext() {
return SecurityContextHolder.getContext();
}
// exposed for testing
protected SignedJWT parseJWT(String serializedJWT) throws ParseException {
return SignedJWT.parse(serializedJWT);
}
/**
* Validates a Knox token with expiration and begin times and verifies the token with a public Knox key.
* @param jwtToken Knox token
* @param userName User name associated with the token
* @return Whether a token is valid or not
* @throws ParseException JWT Token could not be parsed.
*/
protected boolean isValid(SignedJWT jwtToken, String userName) throws ParseException {
// Verify the user name is present
if (userName == null || userName.isEmpty()) {
LOG.info("Could not find user name in SSO token");
return false;
}
Date now = new Date();
// Verify the token has not expired
Date expirationTime = jwtToken.getJWTClaimsSet().getExpirationTime();
if (expirationTime != null && now.after(expirationTime)) {
LOG.info("SSO token expired: {} ", userName);
return false;
}
// Verify the token is not before time
Date notBeforeTime = jwtToken.getJWTClaimsSet().getNotBeforeTime();
if (notBeforeTime != null && now.before(notBeforeTime)) {
LOG.info("SSO token not yet valid: {} ", userName);
return false;
}
return validateSignature(jwtToken);
}
/**
* 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) {
// Verify the token signature algorithm was as expected
String receivedSigAlg = jwtToken.getHeader().getAlgorithm().getName();
if (!receivedSigAlg.equals(JWSAlgorithm.RS256.getName())) {
return false;
}
// Verify the token has been properly signed
if (JWSObject.State.SIGNED == jwtToken.getState()) {
LOG.debug("SSO token is in a SIGNED state");
if (jwtToken.getSignature() != null) {
LOG.debug("SSO token signature is not null");
try {
JWSVerifier verifier = getRSASSAVerifier();
if (jwtToken.verify(verifier)) {
LOG.debug("SSO token has been successfully verified");
return true;
} else {
LOG.warn("SSO signature verification failed. Please check the public key.");
}
} catch (Exception e) {
LOG.warn("Error while validating signature", e);
}
}
}
return false;
}
// exposed for testing
protected RSASSAVerifier getRSASSAVerifier() throws CertificateException, IOException {
return new RSASSAVerifier(SecurityUtils.parseRSAPublicKey(getKnoxKey()));
}
/**
* Encapsulate the acquisition of the JWT token from HTTP cookies within the
* request.
*
* @param req ServletRequest 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) {
LOG.debug(String.format("Found cookie: %s [%s]", cookie.getName(), cookie.getValue()));
if (knoxCookie.equals(cookie.getName())) {
if (LOG.isDebugEnabled()) {
LOG.debug(knoxCookie + " cookie has been found and is being processed");
}
serializedJWT = cookie.getValue();
break;
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(knoxCookie + " not found");
}
}
return serializedJWT;
}
/**
* A public Knox key can either be passed in directly or read from a file.
* @return Public Knox key
* @throws IOException There was a problem reading the Knox key file.
*/
protected String getKnoxKey() throws IOException {
String knoxKey;
if ((this.knoxKeyString == null || this.knoxKeyString.isEmpty()) && this.knoxKeyFile != null) {
List<String> keyLines = Files.readAllLines(knoxKeyFile, StandardCharsets.UTF_8);
knoxKey = String.join("", keyLines);
} else {
knoxKey = this.knoxKeyString;
}
return knoxKey;
}
/**
* Builds the Spring Authentication object using the supplied user name and groups looked up from LDAP. Groups are currently
* mapped directly to Spring roles by converting to upper case and prepending the name with "ROLE_".
* @param userName The username to build the Authentication object with.
* @param httpRequest HttpServletRequest
* @return Authentication object for the given user.
*/
protected Authentication getAuthentication(String userName, HttpServletRequest httpRequest) {
String ldapName = LdapNameBuilder.newInstance().add(userSearchBase).add("uid", userName).build().toString();
// Search ldap for a user's groups and convert to a Spring role
List<GrantedAuthority> grantedAuths = ldapTemplate.search(query()
.where("objectclass")
.is("groupOfNames")
.and("member")
.is(ldapName), (AttributesMapper<String>) attrs -> (String) attrs.get("cn").get())
.stream()
.map(group -> String.format("%s%s", SECURITY_ROLE_PREFIX, group.toUpperCase()))
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
final UserDetails principal = new User(userName, "", grantedAuths);
final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
principal, "", grantedAuths);
WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
authentication.setDetails(webDetails);
return authentication;
}
}