|
@@ -0,0 +1,329 @@
|
|
|
+/*
|
|
|
+ * 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.ambari.server.security.authorization.jwt;
|
|
|
+
|
|
|
+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.apache.ambari.server.configuration.Configuration;
|
|
|
+import org.apache.ambari.server.security.authorization.AmbariGrantedAuthority;
|
|
|
+import org.apache.ambari.server.security.authorization.User;
|
|
|
+import org.apache.ambari.server.security.authorization.UserType;
|
|
|
+import org.apache.ambari.server.security.authorization.Users;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.security.authentication.BadCredentialsException;
|
|
|
+import org.springframework.security.core.Authentication;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
+import org.springframework.security.web.AuthenticationEntryPoint;
|
|
|
+
|
|
|
+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.interfaces.RSAPublicKey;
|
|
|
+import java.text.ParseException;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Filter is used to validate JWT token and authenticate user.
|
|
|
+ * It is also responsive for creating user in local Ambari database for further management
|
|
|
+ */
|
|
|
+public class JwtAuthenticationFilter implements Filter {
|
|
|
+ Logger LOG = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
|
|
+
|
|
|
+ private final JwtAuthenticationProperties jwtProperties;
|
|
|
+
|
|
|
+ private String originalUrlQueryParam = "originalUrl";
|
|
|
+ private String authenticationProviderUrl = null;
|
|
|
+ private RSAPublicKey publicKey = null;
|
|
|
+ private List<String> audiences = null;
|
|
|
+ private String cookieName = "hadoop-jwt";
|
|
|
+
|
|
|
+ private boolean ignoreFailure = true;
|
|
|
+ private AuthenticationEntryPoint entryPoint;
|
|
|
+ private Users users;
|
|
|
+
|
|
|
+ @Inject
|
|
|
+ public JwtAuthenticationFilter(Configuration configuration, AuthenticationEntryPoint entryPoint, Users users) {
|
|
|
+ this.entryPoint = entryPoint;
|
|
|
+ this.users = users;
|
|
|
+ jwtProperties = configuration.getJwtProperties();
|
|
|
+ loadJwtProperties();
|
|
|
+ }
|
|
|
+
|
|
|
+ public JwtAuthenticationFilter(JwtAuthenticationProperties jwtProperties, AuthenticationEntryPoint entryPoint,
|
|
|
+ Users users) {
|
|
|
+ this.jwtProperties = jwtProperties;
|
|
|
+ this.entryPoint = entryPoint;
|
|
|
+ this.users = users;
|
|
|
+ loadJwtProperties();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void init(FilterConfig filterConfig) throws ServletException {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
|
|
+
|
|
|
+ if (jwtProperties != null && isAuthenticationRequired()) {
|
|
|
+ HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
|
|
|
+ HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
|
|
|
+
|
|
|
+ String serializedJWT = getJWTFromCookie(httpServletRequest);
|
|
|
+ if (serializedJWT != null) {
|
|
|
+ SignedJWT jwtToken = null;
|
|
|
+ try {
|
|
|
+ jwtToken = SignedJWT.parse(serializedJWT);
|
|
|
+ boolean valid = validateToken(jwtToken);
|
|
|
+
|
|
|
+ if (valid) {
|
|
|
+ String userName = jwtToken.getJWTClaimsSet().getSubject();
|
|
|
+ User user = users.getUser(userName, UserType.JWT);
|
|
|
+ if (user == null) {
|
|
|
+ // create user in local database on first login, usually we cannot fetch all users
|
|
|
+ // from external authentication provider (as we do during ldap-sync process)
|
|
|
+ users.createUser(userName, null, UserType.JWT, true, false);
|
|
|
+ user = users.getUser(userName, UserType.JWT);
|
|
|
+ }
|
|
|
+
|
|
|
+ Collection<AmbariGrantedAuthority> userAuthorities =
|
|
|
+ users.getUserAuthorities(user.getUserName(), user.getUserType());
|
|
|
+
|
|
|
+ JwtAuthentication token = new JwtAuthentication(jwtToken, user, userAuthorities);
|
|
|
+ token.setAuthenticated(true);
|
|
|
+
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(token);
|
|
|
+
|
|
|
+
|
|
|
+ } else {
|
|
|
+ LOG.warn("JWT authentication failed");
|
|
|
+ if (ignoreFailure) {
|
|
|
+ filterChain.doFilter(servletRequest, servletResponse);
|
|
|
+ } else {
|
|
|
+ //used to indicate authentication failure, not used here as we have more than one filter
|
|
|
+ entryPoint.commence(httpServletRequest, httpServletResponse, new BadCredentialsException("Invalid JWT " +
|
|
|
+ "token"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ } catch (ParseException e) {
|
|
|
+ LOG.warn("Unable to parse the JWT token", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ filterChain.doFilter(servletRequest, servletResponse);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void loadJwtProperties() {
|
|
|
+ if (jwtProperties != null) {
|
|
|
+ authenticationProviderUrl = jwtProperties.getAuthenticationProviderUrl();
|
|
|
+ publicKey = jwtProperties.getPublicKey();
|
|
|
+ audiences = jwtProperties.getAudiences();
|
|
|
+ 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 isAuthenticationRequired() {
|
|
|
+ Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
|
|
|
+ return !(existingAuth != null && existingAuth.isAuthenticated()) || existingAuth instanceof JwtAuthentication;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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();
|
|
|
+ String userName = null;
|
|
|
+ if (cookies != null) {
|
|
|
+ for (Cookie cookie : cookies) {
|
|
|
+ if (cookieName.equals(cookie.getName())) {
|
|
|
+ LOG.info(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().toString();
|
|
|
+ return loginURL;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 could not be verified");
|
|
|
+ }
|
|
|
+ boolean audValid = validateAudiences(jwtToken);
|
|
|
+ if (!audValid) {
|
|
|
+ LOG.warn("Audience validation failed.");
|
|
|
+ }
|
|
|
+ boolean expValid = validateExpiration(jwtToken);
|
|
|
+ if (!expValid) {
|
|
|
+ LOG.info("Expiration validation failed.");
|
|
|
+ }
|
|
|
+
|
|
|
+ return sigValid && audValid && 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()) {
|
|
|
+ LOG.debug("JWT token is in a SIGNED state");
|
|
|
+ if (jwtToken.getSignature() != null) {
|
|
|
+ LOG.debug("JWT token signature is not null");
|
|
|
+ try {
|
|
|
+ JWSVerifier verifier = new RSASSAVerifier(publicKey);
|
|
|
+ if (jwtToken.verify(verifier)) {
|
|
|
+ valid = true;
|
|
|
+ LOG.debug("JWT token has been successfully verified");
|
|
|
+ } else {
|
|
|
+ LOG.warn("JWT signature verification failed.");
|
|
|
+ }
|
|
|
+ } catch (JOSEException je) {
|
|
|
+ LOG.warn("Error while validating signature", je);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return valid;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate whether any of the accepted audience claims is present in the
|
|
|
+ * issued 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(SignedJWT jwtToken) {
|
|
|
+ boolean valid = false;
|
|
|
+ try {
|
|
|
+ List<String> 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
|
|
|
+ boolean found = false;
|
|
|
+ for (String aud : tokenAudienceList) {
|
|
|
+ if (audiences.contains(aud)) {
|
|
|
+ LOG.debug("JWT token audience has been successfully validated");
|
|
|
+ valid = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!valid) {
|
|
|
+ LOG.warn("JWT audience validation failed.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (ParseException pe) {
|
|
|
+ LOG.warn("Unable to parse the JWT token.", pe);
|
|
|
+ }
|
|
|
+ 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)) {
|
|
|
+ LOG.debug("JWT token expiration date has been "
|
|
|
+ + "successfully validated");
|
|
|
+ valid = true;
|
|
|
+ } else {
|
|
|
+ LOG.warn("JWT expiration date validation failed.");
|
|
|
+ }
|
|
|
+ } catch (ParseException pe) {
|
|
|
+ LOG.warn("JWT expiration date validation failed.", pe);
|
|
|
+ }
|
|
|
+ return valid;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void destroy() {
|
|
|
+
|
|
|
+ }
|
|
|
+}
|