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