View Javadoc
1   /*******************************************************************************
2    * Copyright 2012 Internet2
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   ******************************************************************************/
16  /*
17   * @author mchyzer
18   * $Id: WsGrouperKerberosAuthentication.java,v 1.3 2009-04-13 20:24:22 mchyzer Exp $
19   */
20  package edu.internet2.middleware.grouper.ws.security;
21  
22  import java.io.File;
23  import java.util.LinkedHashMap;
24  import java.util.Map;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import javax.security.auth.login.LoginContext;
29  import javax.security.auth.login.LoginException;
30  import javax.servlet.http.HttpServletRequest;
31  
32  import org.apache.commons.codec.binary.Base64;
33  import org.apache.commons.lang.StringUtils;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  
37  import edu.internet2.middleware.grouper.cache.GrouperCache;
38  import edu.internet2.middleware.grouper.j2ee.Authentication;
39  import edu.internet2.middleware.grouper.util.GrouperUtil;
40  import edu.internet2.middleware.grouper.ws.GrouperWsConfig;
41  import edu.internet2.middleware.grouper.ws.util.GrouperServiceUtils;
42  import edu.internet2.middleware.morphString.Morph;
43  
44  
45  /**
46   * <pre>
47   * basic kerberos authentication for grouper, settings are specified in grouper-ws.properties
48   * note: this can be used for rest and soap, though it is not a bastion of security:
49   *  1. for soap, ws-security would be better since a ticket is passed instead of user/pass
50   *  2. for rest, Im not sure there is another option
51   *  3. the user/pass is transmitted in basic auth, so make sure SSL is on
52   *  4. passing the user/pass is not how kerberos should work since kerberos passes tickets and not passes
53   *  5. the user is authenticated to the kdc, but an ssl service is not invoked, which would be the next
54   *  level of verification since it might be possible for the kdc to be spoofed to the grouper-ws
55   * 
56   * </pre>
57   */
58  public class WsGrouperKerberosAuthentication implements WsCustomAuthentication {
59  
60    /**
61     * 
62     * @param args
63     * @throws Exception 
64     */
65    public static void main(String[] args) throws Exception {
66      GrouperUtil.waitForInput();
67      for (int i=0; i<2; i++) {
68        for (int j=0;j<100;j++) {
69          //TODO  put this in external file
70          if (!authenticateKerberos("penngroups/medley.isc-seo.upenn.edu", 
71              Morph.decryptIfFile("R:/home/appadmin/pass/pennGroups/pennGroupsMedley.pass"))) {
72            throw new RuntimeException("Problem!");
73          }
74          System.gc();
75          System.out.println(j + ":" + i + ", " 
76              + ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())/(double)(1024*1024)) + " megs used");
77          Thread.sleep(100);
78        }
79        GrouperUtil.waitForInput();
80      }
81    }
82  
83    /** logger */
84    private static final Log LOG = GrouperUtil.getLog(WsGrouperKerberosAuthentication.class);
85  
86    /**
87     * cache the logins in a hash cache
88     */
89    private static GrouperCache<String, String> loginCache = new GrouperCache<String, String>(
90        WsGrouperKerberosAuthentication.class.getName() + ".userCache", 10000, false, 60*1, 60*1, false);
91  
92    /**
93     * 
94     * @see edu.internet2.middleware.grouper.ws.security.WsCustomAuthentication#retrieveLoggedInSubjectId(javax.servlet.http.HttpServletRequest)
95     */
96    public String retrieveLoggedInSubjectId(HttpServletRequest httpServletRequest)
97        throws RuntimeException {
98      
99      String authHeader = httpServletRequest.getHeader("Authorization");
100 
101     //if not header, we cant go to kerberos
102     if (StringUtils.isBlank(authHeader)) {
103       LOG.error("No authorization header in HTTP");
104       return null;
105     }
106     
107     //hash the authHeader
108     String authHeaderHash = GrouperUtil.encryptSha(authHeader);
109     
110     String cachedLogin = loginCache.get(authHeaderHash);
111     if (!StringUtils.isBlank(cachedLogin)) {
112       LOG.debug("Retrieved cached login");
113       return cachedLogin;
114     }
115     LOG.debug("Login not in cache");
116     
117     String user = Authentication.retrieveUsername(authHeader);
118     String pass = Authentication.retrievePassword(authHeader);
119     
120     if (authenticateKerberos(user, pass)) {
121       
122       loginCache.put(authHeaderHash, user);
123       
124       return user;
125     }
126     
127     LOG.error("Error authenticating user: " + user);
128     return null;
129   }
130 
131   /**
132    * return something like 1ms to troubleshoot time issues
133    * @param startNanos
134    * @return the millis
135    */
136   private static String timeMillis(long startNanos) {
137     return ((System.nanoTime() - startNanos) / 1000000) + "ms";
138   }
139   
140   /**
141    * see if a user and pass are correct with berberos
142    * @param principal
143    * @param password
144    * @return true for ok, false for not
145    */
146   public static boolean authenticateKerberos(String principal, String password) {
147 
148     Map<String, Object> debugMap = LOG.isDebugEnabled() ? new LinkedHashMap<String, Object>() : null;
149     long startNanos = System.nanoTime();
150     
151     try {
152 
153       if (LOG.isDebugEnabled()) {
154         debugMap.put("method", "authenticateKerberos()");
155       }
156   
157       // Obtain a LoginContext, needed for authentication. Tell it 
158       // to use the LoginModule implementation specified by the 
159       // entry named "JaasSample" in the JAAS login configuration 
160       // file and to also use the specified CallbackHandler.
161   
162       File jaasConf = GrouperServiceUtils.fileFromResourceName("jaas.conf");
163   
164       if (LOG.isDebugEnabled()) {
165         debugMap.put("jaasConfFound", jaasConf != null);
166         debugMap.put("jaasConfLocation", jaasConf == null ? null : jaasConf.getAbsolutePath());
167       }
168   
169       if (jaasConf == null) {
170         throw new RuntimeException("Cant find jaas.conf!");
171       }
172   
173       String krb5Location = GrouperWsConfig.retrieveConfig().propertyValueString("kerberos.krb5.conf.location");
174   
175       if (LOG.isDebugEnabled()) {
176         debugMap.put("krb5Location", krb5Location);
177       }
178   
179       File krb5confFile = null;
180       
181       //first look for external central file on OS
182       if (!StringUtils.isBlank(krb5Location)) {
183         krb5confFile = new File(krb5Location);
184   
185         if (LOG.isDebugEnabled()) {
186           debugMap.put("krb5confFile", krb5confFile.getAbsolutePath());
187           debugMap.put("krb5confFileFound", krb5confFile.exists() || krb5confFile.isFile());
188         }
189   
190         if (!krb5confFile.exists() || !krb5confFile.isFile()) {
191           throw new RuntimeException("krb5 conf file in " + krb5Location + " does not exist or is not a file");
192         }
193       } else {
194            
195          krb5confFile = GrouperUtil.fileFromResourceName("krb5.conf"); 
196   
197          if (LOG.isDebugEnabled()) {
198            debugMap.put("krb5confFile", krb5confFile == null ? null : krb5confFile.getAbsolutePath());
199            debugMap.put("krb5confFileFound", krb5confFile.exists() || krb5confFile.isFile());
200          }
201       }
202       
203       if (krb5confFile == null) { 
204         if (LOG.isDebugEnabled()) {
205           debugMap.put("krb5confFileNotFoundFound", true);
206           debugMap.put("kerberos.realm", GrouperWsConfig.retrieveConfig().propertyValueString("kerberos.realm"));
207           debugMap.put("kerberos.kdc.address", GrouperWsConfig.retrieveConfig().propertyValueString("kerberos.kdc.address"));
208         }
209         
210         System.setProperty("java.security.krb5.realm", GrouperWsConfig.retrieveConfig().propertyValueStringRequired("kerberos.realm"));
211         System.setProperty("java.security.krb5.kdc", GrouperWsConfig.retrieveConfig().propertyValueStringRequired("kerberos.kdc.address"));
212       } else {
213         
214         System.setProperty("java.security.krb5.conf", krb5confFile.getAbsolutePath()); 
215       }
216    
217       
218       System.setProperty("java.security.auth.login.config", jaasConf.getAbsolutePath());
219       
220       // # debug kerberos, sets system property sun.security.krb5.debug = true
221       // # {valueType: "boolean"}
222       // kerberos.debug = false
223       if (GrouperWsConfig.retrieveConfig().propertyValueBoolean("kerberos.debug", false)) {
224         if (LOG.isDebugEnabled()) {
225           debugMap.put("kerberos.debug", true);
226         }
227         
228         System.setProperty("sun.security.krb5.debug", "true");
229       }
230       
231       LoginContext lc = null;
232       try {
233         lc = new LoginContext("JaasSample", new GrouperWsKerberosHandler(principal, password));
234         
235         if (LOG.isDebugEnabled()) {
236           debugMap.put("loginContextCreated", true + " " + timeMillis(startNanos));
237         }
238   
239       } catch (LoginException le) {
240         if (LOG.isDebugEnabled()) {
241           debugMap.put("errorCreatingLoginContext", true + " " + timeMillis(startNanos));
242         }
243         LOG.error("Cannot create LoginContext. ", le);
244         return false;
245       } catch (SecurityException se) {
246         if (LOG.isDebugEnabled()) {
247           debugMap.put("errorCreatingLoginContext", true + " " + timeMillis(startNanos));
248         }
249         LOG.error("Cannot create LoginContext. " , se);
250         return false;
251       }
252   
253       try {
254   
255         // attempt authentication
256         lc.login();
257   
258         if (LOG.isDebugEnabled()) {
259           debugMap.put("loggedIn", true + " " + timeMillis(startNanos));
260         }
261   
262         try {
263           lc.logout();
264           if (LOG.isDebugEnabled()) {
265             debugMap.put("loggedOut", true + " " + timeMillis(startNanos));
266           }
267         } catch (Exception e) {
268           LOG.warn(e);
269         }
270         return true;
271       } catch (LoginException le) {
272         
273         if (LOG.isDebugEnabled()) {
274           debugMap.put("loginException", true + " " + timeMillis(startNanos));
275         }
276         LOG.warn(le);
277       }
278     } catch (RuntimeException re) {
279       
280       if (LOG.isDebugEnabled()) {
281         debugMap.put("took", timeMillis(startNanos));
282       }
283     } finally {
284       if (LOG.isDebugEnabled()) {
285         debugMap.put("took", timeMillis(startNanos));
286         LOG.debug(GrouperUtil.mapToString(debugMap));
287       }
288     }
289     return false;
290   }
291 
292   
293 }