metron-issues mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From "ASF GitHub Bot (JIRA)" <j...@apache.org>
Subject [jira] [Commented] (METRON-1895) Add Knox SSO as an option in Metron
Date Mon, 03 Dec 2018 13:51:00 GMT

    [ https://issues.apache.org/jira/browse/METRON-1895?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16707224#comment-16707224
] 

ASF GitHub Bot commented on METRON-1895:
----------------------------------------

Github user justinleet commented on a diff in the pull request:

    https://github.com/apache/metron/pull/1281#discussion_r237918925
  
    --- Diff: 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";
    +    String PEM_FOOTER = "\n-----END CERTIFICATE-----";
    +    String fullPem = (pem.startsWith(PEM_HEADER) && pem.endsWith(PEM_FOOTER))
? pem : 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 CertificateException(message, ce);
    +    }
    +    return (RSAPublicKey) key;
    +  }
    +
    +  /**
    +   * 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
    +   * @param httpRequest
    +   * @return
    +   */
    +  private 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("ROLE_%s", group.toUpperCase()))
    +            .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    +
    +    final UserDetails principal = new User(userName, "", grantedAuths);
    +    final Authentication authentication = new UsernamePasswordAuthenticationToken(
    +            principal, "", grantedAuths);
    +    WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
    +    ((AbstractAuthenticationToken) authentication).setDetails(webDetails);
    --- End diff --
    
    You could kill this awkward cast by just leaving `Authentication authentication` as `UsernamePasswordAuthenticationToken
authentication`, since you're using an implementation level method.
    
    It would just become
    ```
        final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                principal, "", grantedAuths);
        WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
        authentication.setDetails(webDetails);
    ```


> Add Knox SSO as an option in Metron
> -----------------------------------
>
>                 Key: METRON-1895
>                 URL: https://issues.apache.org/jira/browse/METRON-1895
>             Project: Metron
>          Issue Type: New Feature
>            Reporter: Ryan Merriman
>            Priority: Major
>
> This feature will enable accessing Metron REST and the UIs through Knox's SSO mechanism.



--
This message was sent by Atlassian JIRA
(v7.6.3#76005)

Mime
View raw message