1 /** 2 * Licensed under the Apache License, Version 2.0 (the "License"); 3 * you may not use this file except in compliance with the License. 4 * You may obtain a copy of the License at 5 * 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * 8 * Unless required by applicable law or agreed to in writing, software 9 * distributed under the License is distributed on an "AS IS" BASIS, 10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 * See the License for the specific language governing permissions and 12 * limitations under the License. See accompanying LICENSE file. 13 */ 14 package org.apache.hadoop.security.authentication.server; 15 16 import org.apache.hadoop.security.authentication.client.AuthenticationException; 17 import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; 18 import org.apache.commons.codec.binary.Base64; 19 import org.apache.hadoop.security.KerberosName; 20 import org.apache.hadoop.security.authentication.util.KerberosUtil; 21 import org.ietf.jgss.GSSContext; 22 import org.ietf.jgss.GSSCredential; 23 import org.ietf.jgss.GSSManager; 24 import org.slf4j.Logger; 25 import org.slf4j.LoggerFactory; 26 27 import javax.security.auth.Subject; 28 import javax.security.auth.kerberos.KerberosPrincipal; 29 import javax.security.auth.login.AppConfigurationEntry; 30 import javax.security.auth.login.Configuration; 31 import javax.security.auth.login.LoginContext; 32 import javax.security.auth.login.LoginException; 33 import javax.servlet.ServletException; 34 import javax.servlet.http.HttpServletRequest; 35 import javax.servlet.http.HttpServletResponse; 36 import java.io.File; 37 import java.io.IOException; 38 import java.security.Principal; 39 import java.security.PrivilegedActionException; 40 import java.security.PrivilegedExceptionAction; 41 import java.util.HashMap; 42 import java.util.HashSet; 43 import java.util.Map; 44 import java.util.Properties; 45 import java.util.Set; 46 47 /** 48 * The {@link KerberosAuthenticationHandler} implements the Kerberos SPNEGO authentication mechanism for HTTP. 49 * <p/> 50 * The supported configuration properties are: 51 * <ul> 52 * <li>kerberos.principal: the Kerberos principal to used by the server. As stated by the Kerberos SPNEGO 53 * specification, it should be <code>HTTP/${HOSTNAME}@{REALM}</code>. The realm can be omitted from the 54 * principal as the JDK GSS libraries will use the realm name of the configured default realm. 55 * It does not have a default value.</li> 56 * <li>kerberos.keytab: the keytab file containing the credentials for the Kerberos principal. 57 * It does not have a default value.</li> 58 * </ul> 59 */ 60 public class KerberosAuthenticationHandler implements AuthenticationHandler { 61 private static Logger LOG = LoggerFactory.getLogger(KerberosAuthenticationHandler.class); 62 63 /** 64 * Kerberos context configuration for the JDK GSS library. 65 */ 66 private static class KerberosConfiguration extends Configuration { 67 private String keytab; 68 private String principal; 69 KerberosConfiguration(String keytab, String principal)70 public KerberosConfiguration(String keytab, String principal) { 71 this.keytab = keytab; 72 this.principal = principal; 73 } 74 75 @Override getAppConfigurationEntry(String name)76 public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 77 Map<String, String> options = new HashMap<String, String>(); 78 options.put("keyTab", keytab); 79 options.put("principal", principal); 80 options.put("useKeyTab", "true"); 81 options.put("storeKey", "true"); 82 options.put("doNotPrompt", "true"); 83 options.put("useTicketCache", "true"); 84 options.put("renewTGT", "true"); 85 options.put("refreshKrb5Config", "true"); 86 options.put("isInitiator", "false"); 87 String ticketCache = System.getenv("KRB5CCNAME"); 88 if (ticketCache != null) { 89 options.put("ticketCache", ticketCache); 90 } 91 if (LOG.isDebugEnabled()) { 92 options.put("debug", "true"); 93 } 94 95 return new AppConfigurationEntry[]{ 96 new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(), 97 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, 98 options),}; 99 } 100 } 101 102 /** 103 * Constant that identifies the authentication mechanism. 104 */ 105 public static final String TYPE = "kerberos"; 106 107 /** 108 * Constant for the configuration property that indicates the kerberos principal. 109 */ 110 public static final String PRINCIPAL = TYPE + ".principal"; 111 112 /** 113 * Constant for the configuration property that indicates the keytab file path. 114 */ 115 public static final String KEYTAB = TYPE + ".keytab"; 116 117 /** 118 * Constant for the configuration property that indicates the Kerberos name 119 * rules for the Kerberos principals. 120 */ 121 public static final String NAME_RULES = TYPE + ".name.rules"; 122 123 private String principal; 124 private String keytab; 125 private GSSManager gssManager; 126 private LoginContext loginContext; 127 128 /** 129 * Initializes the authentication handler instance. 130 * <p/> 131 * It creates a Kerberos context using the principal and keytab specified in the configuration. 132 * <p/> 133 * This method is invoked by the {@link AuthenticationFilter#init} method. 134 * 135 * @param config configuration properties to initialize the handler. 136 * 137 * @throws ServletException thrown if the handler could not be initialized. 138 */ 139 @Override init(Properties config)140 public void init(Properties config) throws ServletException { 141 try { 142 principal = config.getProperty(PRINCIPAL, principal); 143 if (principal == null || principal.trim().length() == 0) { 144 throw new ServletException("Principal not defined in configuration"); 145 } 146 keytab = config.getProperty(KEYTAB, keytab); 147 if (keytab == null || keytab.trim().length() == 0) { 148 throw new ServletException("Keytab not defined in configuration"); 149 } 150 if (!new File(keytab).exists()) { 151 throw new ServletException("Keytab does not exist: " + keytab); 152 } 153 154 Set<Principal> principals = new HashSet<Principal>(); 155 principals.add(new KerberosPrincipal(principal)); 156 Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>()); 157 158 KerberosConfiguration kerberosConfiguration = new KerberosConfiguration(keytab, principal); 159 160 LOG.info("Login using keytab "+keytab+", for principal "+principal); 161 loginContext = new LoginContext("", subject, null, kerberosConfiguration); 162 loginContext.login(); 163 164 Subject serverSubject = loginContext.getSubject(); 165 try { 166 gssManager = Subject.doAs(serverSubject, new PrivilegedExceptionAction<GSSManager>() { 167 168 @Override 169 public GSSManager run() throws Exception { 170 return GSSManager.getInstance(); 171 } 172 }); 173 } catch (PrivilegedActionException ex) { 174 throw ex.getException(); 175 } 176 LOG.info("Initialized, principal [{}] from keytab [{}]", principal, keytab); 177 } catch (Exception ex) { 178 throw new ServletException(ex); 179 } 180 } 181 182 /** 183 * Releases any resources initialized by the authentication handler. 184 * <p/> 185 * It destroys the Kerberos context. 186 */ 187 @Override destroy()188 public void destroy() { 189 try { 190 if (loginContext != null) { 191 loginContext.logout(); 192 loginContext = null; 193 } 194 } catch (LoginException ex) { 195 LOG.warn(ex.getMessage(), ex); 196 } 197 } 198 199 /** 200 * Returns the authentication type of the authentication handler, 'kerberos'. 201 * <p/> 202 * 203 * @return the authentication type of the authentication handler, 'kerberos'. 204 */ 205 @Override getType()206 public String getType() { 207 return TYPE; 208 } 209 210 /** 211 * Returns the Kerberos principal used by the authentication handler. 212 * 213 * @return the Kerberos principal used by the authentication handler. 214 */ getPrincipal()215 protected String getPrincipal() { 216 return principal; 217 } 218 219 /** 220 * Returns the keytab used by the authentication handler. 221 * 222 * @return the keytab used by the authentication handler. 223 */ getKeytab()224 protected String getKeytab() { 225 return keytab; 226 } 227 228 /** 229 * It enforces the the Kerberos SPNEGO authentication sequence returning an {@link AuthenticationToken} only 230 * after the Kerberos SPNEGO sequence has completed successfully. 231 * <p/> 232 * 233 * @param request the HTTP client request. 234 * @param response the HTTP client response. 235 * 236 * @return an authentication token if the Kerberos SPNEGO sequence is complete and valid, 237 * <code>null</code> if it is in progress (in this case the handler handles the response to the client). 238 * 239 * @throws IOException thrown if an IO error occurred. 240 * @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed. 241 */ 242 @Override authenticate(HttpServletRequest request, final HttpServletResponse response)243 public AuthenticationToken authenticate(HttpServletRequest request, final HttpServletResponse response) 244 throws IOException, AuthenticationException { 245 AuthenticationToken token = null; 246 String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION); 247 248 if (authorization == null || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) { 249 response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE); 250 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 251 if (authorization == null) { 252 LOG.trace("SPNEGO starting"); 253 } else { 254 LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION + "' does not start with '" + 255 KerberosAuthenticator.NEGOTIATE + "' : {}", authorization); 256 } 257 } else { 258 authorization = authorization.substring(KerberosAuthenticator.NEGOTIATE.length()).trim(); 259 final Base64 base64 = new Base64(0); 260 final byte[] clientToken = base64.decode(authorization); 261 Subject serverSubject = loginContext.getSubject(); 262 try { 263 token = Subject.doAs(serverSubject, new PrivilegedExceptionAction<AuthenticationToken>() { 264 265 @Override 266 public AuthenticationToken run() throws Exception { 267 AuthenticationToken token = null; 268 GSSContext gssContext = null; 269 try { 270 gssContext = gssManager.createContext((GSSCredential) null); 271 byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length); 272 if (serverToken != null && serverToken.length > 0) { 273 String authenticate = base64.encodeToString(serverToken); 274 response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, 275 KerberosAuthenticator.NEGOTIATE + " " + authenticate); 276 } 277 if (!gssContext.isEstablished()) { 278 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 279 LOG.trace("SPNEGO in progress"); 280 } else { 281 String clientPrincipal = gssContext.getSrcName().toString(); 282 KerberosName kerberosName = new KerberosName(clientPrincipal); 283 String userName = kerberosName.getShortName(); 284 token = new AuthenticationToken(userName, clientPrincipal, TYPE); 285 response.setStatus(HttpServletResponse.SC_OK); 286 LOG.trace("SPNEGO completed for principal [{}]", clientPrincipal); 287 } 288 } finally { 289 if (gssContext != null) { 290 gssContext.dispose(); 291 } 292 } 293 return token; 294 } 295 }); 296 } catch (PrivilegedActionException ex) { 297 if (ex.getException() instanceof IOException) { 298 throw (IOException) ex.getException(); 299 } 300 else { 301 throw new AuthenticationException(ex.getException()); 302 } 303 } 304 } 305 return token; 306 } 307 308 } 309