You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@metron.apache.org by GitBox <gi...@apache.org> on 2018/12/13 20:55:20 UTC

[GitHub] justinleet commented on a change in pull request #1281: METRON-1895: Add Knox SSO as an option in Metron

justinleet commented on a change in pull request #1281: METRON-1895:  Add Knox SSO as an option in Metron
URL: https://github.com/apache/metron/pull/1281#discussion_r241557962
 
 

 ##########
 File path: metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java
 ##########
 @@ -0,0 +1,314 @@
+/**
+ * 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.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.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.support.LdapNameBuilder;
+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.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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+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) throws IOException, CertificateException {
+    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) throws ServletException {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  /**
+   * Extracts the Knox token from the configured cookie.  If basic authentication headers are present, SSO authentication
+   * is skipped.
+   * @param request
+   * @param response
+   * @param chain
+   * @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 = null;
+        try {
+          jwtToken = SignedJWT.parse(serializedJWT);
+          String userName = jwtToken.getJWTClaimsSet().getSubject();
+          LOG.info("SSO login user : {} ", userName);
+          if (isValid(jwtToken, userName)) {
+            Authentication authentication = getAuthentication(userName, httpRequest);
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+          }
+        } catch (ParseException e) {
+          LOG.warn("Unable to parse the JWT token", e);
+        }
+      }
+    }
+    chain.doFilter(request, response);
+  }
+
+  /**
+   * 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
+   */
+  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("RS256")) {
+      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 = new RSASSAVerifier(parseRSAPublicKey(getKnoxKey()));
+          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;
+  }
+
+  /**
+   * Encapsulate the acquisition of the JWT token from HTTP cookies within the
+   * request.
+   *
+   * Taken from
+   *
+   * @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) {
+        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
+   */
+  private 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);
+      if (keyLines != null) {
+        knoxKey = String.join("", keyLines);
+      } else {
+        knoxKey = "";
+      }
+    } else {
+      knoxKey = this.knoxKeyString;
+    }
+    return knoxKey;
+  }
+
+  public static RSAPublicKey parseRSAPublicKey(String pem)
+          throws CertificateException, UnsupportedEncodingException {
+    String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n";
 
 Review comment:
   Yeah, I'm fine with that. It's a bit annoying, but if there's not a library to handle this sort of thing, it is what it is.

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services