/** * */ package com.thinkbiganalytics.security.auth.kerberos; /*- * #%L * kylo-security-auth-kerberos * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed 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. * #L% */ import com.thinkbiganalytics.auth.UsernameAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.crypto.codec.Base64; import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken; import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; import java.io.IOException; import java.io.UnsupportedEncodingException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Performs SPNEGO validation of the Kerberos ticket and, if valid, performs further authentication * of the user name using a UsernamePasswordAuthenticationToken with an empty password. */ public class SpnegoValidationUserAuthenticationFilter extends GenericFilterBean { private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationManager authenticationManager; private RememberMeServices rememberMeServices = new NullRememberMeServices(); private boolean skipIfAlreadyAuthenticated = true; /* (non-Javadoc) * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) request; HttpServletResponse httpResp = (HttpServletResponse) response; if (skipIfAlreadyAuthenticated) { Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); if (existingAuth != null && existingAuth.isAuthenticated() && (existingAuth instanceof AnonymousAuthenticationToken) == false) { chain.doFilter(httpReq, httpResp); return; } } String header = httpReq.getHeader("Authorization"); if (isTicketHeader(header)) { KerberosServiceRequestToken token = extractTicketToken(header, httpReq); Authentication kerberosAuth; try { kerberosAuth = authenticationManager.authenticate(token); } catch (AuthenticationException e) { // Most likely a wrong configuration on the server side, such as the wrong service principal is configured or // it is not present in the keytab. logger.warn("Failed to validate negotiate header: " + header, e); SecurityContextHolder.clearContext(); httpResp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); httpResp.flushBuffer(); return; } // If the ticket has been authenticated then attempt to authenticate the username. try { UsernameAuthenticationToken userToken = extractUsernameToken(kerberosAuth); Authentication usernameAuth = authenticationManager.authenticate(userToken); SecurityContextHolder.getContext().setAuthentication(usernameAuth); this.rememberMeServices.loginSuccess(httpReq, httpResp, usernameAuth); } catch (AuthenticationException e) { this.rememberMeServices.loginFail(httpReq, httpResp); // httpResp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + e.getMessage()); } } chain.doFilter(httpReq, httpResp); } /** * The authentication manager for validating the ticket. * * @param authenticationManager the authentication manager */ public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * Should Kerberos authentication be skipped if a user is already authenticated * for this request (e.g. in the HTTP session). * * @param skipIfAlreadyAuthenticated default is true */ public void setSkipIfAlreadyAuthenticated(boolean skipIfAlreadyAuthenticated) { this.skipIfAlreadyAuthenticated = skipIfAlreadyAuthenticated; } /** * Sets the authentication details source. * * @param authenticationDetailsSource the authentication details source */ public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } /** * Sets the remember-me service to invoke upon successful for failed authentication. * * @param rememberMeServices the service */ public void setRememberMeServices(RememberMeServices rememberMeServices) { Assert.notNull(rememberMeServices, "rememberMeServices cannot be null"); this.rememberMeServices = rememberMeServices; } @Override public void afterPropertiesSet() { Assert.notNull(authenticationManager, "authenticationManager must be specified"); Assert.notNull(rememberMeServices, "rememberMeServices must be specified"); } protected KerberosServiceRequestToken extractTicketToken(String header, HttpServletRequest request) throws UnsupportedEncodingException { if (logger.isDebugEnabled()) { logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header); } byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8"); byte[] kerberosTicket = Base64.decode(base64Token); KerberosServiceRequestToken token = new KerberosServiceRequestToken(kerberosTicket); token.setDetails(authenticationDetailsSource.buildDetails(request)); return token; } protected boolean isTicketHeader(String header) { return header != null && (header.startsWith("Negotiate ") || header.startsWith("Kerberos ")); } protected UsernameAuthenticationToken extractUsernameToken(Authentication kerberosAuth) { String krbUser = ((User) kerberosAuth.getPrincipal()).getUsername(); int realmStart = krbUser.lastIndexOf('@'); String username = krbUser.substring(0, realmStart == -1 ? krbUser.length() : realmStart); return new UsernameAuthenticationToken(username); } }