001package com.hfg.webapp.filter.auth;
002
003
004import java.io.ByteArrayInputStream;
005import java.io.File;
006import java.io.IOException;
007import java.io.UnsupportedEncodingException;
008import java.net.URLDecoder;
009import java.security.KeyPair;
010import java.security.PrivateKey;
011import java.security.PublicKey;
012import java.util.Arrays;
013import java.util.Base64;
014import java.util.HashSet;
015import java.util.Set;
016import java.util.logging.Level;
017import java.util.logging.Logger;
018import java.util.regex.Pattern;
019
020import javax.servlet.Filter;
021import javax.servlet.FilterChain;
022import javax.servlet.FilterConfig;
023import javax.servlet.ServletException;
024import javax.servlet.ServletRequest;
025import javax.servlet.ServletResponse;
026import javax.servlet.http.Cookie;
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpServletResponse;
029import javax.servlet.http.HttpSession;
030
031import com.hfg.security.CredentialsMgr;
032import com.hfg.security.LoginCredentials;
033import com.hfg.units.TimeSpan;
034import com.hfg.units.TimeUnit;
035import com.hfg.util.*;
036import com.hfg.webapp.HfgCookie;
037import com.hfg.xml.XMLException;
038import com.hfg.xml.XMLTag;
039
040//------------------------------------------------------------------------------
041/**
042 Base class for LDAP_AuthenticationFilter and GooglePlusAuthenticationFilter.
043 <div>
044  @author J. Alex Taylor, hairyfatguy.com
045 </div>
046*/
047//------------------------------------------------------------------------------
048// com.hfg XML/HTML Coding Library
049//
050// This library is free software; you can redistribute it and/or
051// modify it under the terms of the GNU Lesser General Public
052// License as published by the Free Software Foundation; either
053// version 2.1 of the License, or (at your option) any later version.
054//
055// This library is distributed in the hope that it will be useful,
056// but WITHOUT ANY WARRANTY; without even the implied warranty of
057// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
058// Lesser General Public License for more details.
059//
060// You should have received a copy of the GNU Lesser General Public
061// License along with this library; if not, write to the Free Software
062// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
063//
064// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
065// jataylor@hairyfatguy.com
066//------------------------------------------------------------------------------
067
068public abstract class AuthenticationFilter implements Filter
069{
070   private FilterConfig mFilterConfig;
071   private String       mAppName;
072   private String       mInitialUrl;
073   private boolean      mPreserveRequestedUrl = true;
074   private boolean      mUseSessionCookie;
075   private KeyPair      mECKeyPair;
076   private Pattern      mBypassPattern;
077   private Set<String>  mAllowedUserDomains;
078
079   // Session attribute keys
080   public  static final String SESSION_USER = "SESSION_USER";
081   public  static final String STATE        = "STATE";
082   public  static final String REDIRECT_URL = "REDIRECT_URL";
083
084   public  static final String AUTH_FILTER  = "authFilter";
085
086   public static final  String BROWSER_SESSION_AUTH_COOKIE_NAME = "com.hfg.auth.session";
087   public static final  String AUTH_USERNAME_COOKIE_NAME = "com.hfg.auth.username";
088
089   protected static final String USERNAME  = "username";
090   protected static final String PASSWORD  = "password";
091   protected static final String DOMAIN    = "domain";
092   protected static final String TIMEZONE_OFFSET  = "timezoneOffset";
093   protected static final String REMEMBER_USERNAME  = "rememberUsername";
094
095
096   private static final String ApplicationName    = "ApplicationName";
097   private static final String InitialUrl         = "InitialUrl";
098   private static final String PreserveRequestedUrl = "PreserveRequestedUrl";
099   private static final String UseSessionCookie   = "UseSessionCookie";
100   private static final String ECKeyDir           = "ECKeyDir";
101   private static final String BypassRegexp       = "BypassRegexp";
102   private static final String DomainRestriction  = "DomainRestriction";
103
104   private static final Logger LOGGER = Logger.getLogger(AuthenticationFilter.class.getPackage().getName());
105
106   //###########################################################################
107   // PUBLIC METHODS
108   //###########################################################################
109
110   //---------------------------------------------------------------------------
111   public static UserImpl getUser(HttpServletRequest inRequest)
112   {
113      if (null == inRequest)
114      {
115         throw new RuntimeException("Null request provided!");
116      }
117
118      return getUser(inRequest.getSession());
119   }
120
121   //---------------------------------------------------------------------------
122   public static UserImpl getUser(HttpSession inSession)
123   {
124      if (null == inSession)
125      {
126         throw new RuntimeException("Null session provided!");
127      }
128
129      return (UserImpl) inSession.getAttribute(SESSION_USER);
130   }
131
132   //---------------------------------------------------------------------------
133   public void init(FilterConfig inFilterConfig)
134         throws ServletException
135   {
136      mFilterConfig = inFilterConfig;
137
138      // Save a reference to this filter in the servlet context. This can be used
139      // to provide filter-specific logout instructions.
140      mFilterConfig.getServletContext().setAttribute(AUTH_FILTER, this);
141
142      mInitialUrl = inFilterConfig.getInitParameter(InitialUrl);
143      if (! StringUtil.isSet(mInitialUrl))
144      {
145         String msg = "No " + InitialUrl + " init-param defined in the web.xml!";
146         LOGGER.log(Level.SEVERE, msg);
147         throw new RuntimeException(msg);
148      }
149
150
151      String valueString = inFilterConfig.getInitParameter(PreserveRequestedUrl);
152      if (StringUtil.isSet(valueString))
153      {
154         mPreserveRequestedUrl = BooleanUtil.valueOf(valueString.trim());
155      }
156
157
158      mAppName = inFilterConfig.getInitParameter(ApplicationName);
159      if (! StringUtil.isSet(mAppName))
160      {
161         String msg = "No " + ApplicationName + " init-param defined in the web.xml!";
162         LOGGER.log(Level.SEVERE, msg);
163         throw new RuntimeException(msg);
164      }
165
166      // Optionally, a browser session cookie can be used
167      mUseSessionCookie = BooleanUtil.valueOf(inFilterConfig.getInitParameter(UseSessionCookie));
168      if (mUseSessionCookie)
169      {
170         File ecKeyDir = new File(System.getProperty("user.home"));
171         // Optionally, a directory containing an elliptic curve key pair for signing
172         // a browser session cookie can be specified
173         String ecKeyDirPath = inFilterConfig.getInitParameter(ECKeyDir);
174         if (StringUtil.isSet(ecKeyDirPath))
175         {
176            ecKeyDir = new File(ecKeyDirPath.trim());
177         }
178
179         mECKeyPair = getECKeyPair(ecKeyDir);
180
181         if (mECKeyPair != null)
182         {
183            LOGGER.info("LDAP Authorization Filter configured to use EC-signed session cookie");
184         }
185      }
186
187      // Optionally, a regexp to use for authentication bypass can be specified
188      String bypassRegexp = inFilterConfig.getInitParameter(BypassRegexp);
189      if (StringUtil.isSet(bypassRegexp))
190      {
191         mBypassPattern = Pattern.compile(bypassRegexp.trim());
192      }
193
194      String domainRestriction = inFilterConfig.getInitParameter(DomainRestriction);
195      if (StringUtil.isSet(domainRestriction))
196      {
197         String[] values = domainRestriction.trim().split("(\\s|;|,)\\s*");
198         mAllowedUserDomains = new HashSet<>(Arrays.asList(values));
199      }
200   }
201
202   //---------------------------------------------------------------------------
203   @Override
204   public void doFilter(ServletRequest inRequest, ServletResponse inResponse,
205                        FilterChain inFilterChain)
206         throws IOException, ServletException
207   {
208       if (getFilterConfig() != null)
209       {
210           try
211           {
212               innerDoFilter(inRequest, inResponse, inFilterChain);
213           }
214           catch (ServletException e)
215           {
216              throw e;
217           }
218           catch (Exception e)
219           {
220              throw new ServletException(e);
221           }
222       }
223   }
224
225   //---------------------------------------------------------------------------
226   public void logout(HttpServletRequest inServletRequest, HttpServletResponse inServletResponse)
227         throws Exception
228   {
229      HttpSession session = inServletRequest.getSession();
230
231      session.removeAttribute(SESSION_USER);
232
233      // Expire the cookie. (Won't hurt if it's not in use)
234      Cookie cookie = new Cookie(BROWSER_SESSION_AUTH_COOKIE_NAME, "");
235      cookie.setPath("/");
236      cookie.setMaxAge(0); // Expire now
237      inServletResponse.addCookie(cookie);
238   }
239
240   //---------------------------------------------------------------------------
241   public static HfgCookie getBrowserSessionCookie(HttpServletRequest inRequest)
242   {
243      HfgCookie authCookie = null;
244      Cookie[] cookies = inRequest.getCookies();
245      if (cookies != null
246          && cookies.length > 0)
247      {
248         for (Cookie cookie : cookies)
249         {
250            if (cookie.getName().equals(BROWSER_SESSION_AUTH_COOKIE_NAME))
251            {
252               authCookie = new HfgCookie(cookie);
253               break;
254            }
255         }
256      }
257
258      return authCookie;
259   }
260
261   //---------------------------------------------------------------------------
262   public FilterConfig getFilterConfig()
263   {
264      return mFilterConfig;
265   }
266
267   //---------------------------------------------------------------------------
268   public String getInitialURL()
269   {
270      return mInitialUrl;
271   }
272
273   //---------------------------------------------------------------------------
274   public boolean getPreserveRequestedURL()
275   {
276      return mPreserveRequestedUrl;
277   }
278
279   //---------------------------------------------------------------------------
280   public String getAppName()
281   {
282      return mAppName;
283   }
284
285   //---------------------------------------------------------------------------
286   public boolean getUseSessionCookie()
287   {
288      return mUseSessionCookie;
289   }
290
291   //---------------------------------------------------------------------------
292   public KeyPair getECKeyPair()
293   {
294      return mECKeyPair;
295   }
296
297   //---------------------------------------------------------------------------
298   public Pattern getBypassPattern()
299   {
300      return mBypassPattern;
301   }
302
303   //---------------------------------------------------------------------------
304   public Set<String> getAllowedUserDomains()
305   {
306      return mAllowedUserDomains;
307   }
308
309   //---------------------------------------------------------------------------
310   public boolean isAllowedUserDomain(String inValue)
311   {
312      return null == mAllowedUserDomains || mAllowedUserDomains.contains(inValue);
313   }
314
315   //###########################################################################
316   // PROTECTED METHODS
317   //###########################################################################
318
319   //---------------------------------------------------------------------------
320   protected abstract void innerDoFilter(ServletRequest inRequest, ServletResponse inResponse,
321                                FilterChain inFilterChain)
322         throws Exception;
323
324   //---------------------------------------------------------------------------
325   protected static KeyPair getECKeyPair(File inKeyDir)
326   {
327      KeyPair keyPair = null;
328
329      boolean createNewKeys = false;
330      File keyDir = inKeyDir;
331      if (! keyDir.exists())
332      {
333         LOGGER.warning("The specified " + ECKeyDir + ", " + StringUtil.singleQuote(inKeyDir.getPath()) + " does not exist!");
334         createNewKeys = true;
335      }
336      else
337      {
338         File publicKeyFile = new File(keyDir, "ECPublic.key");
339         if (! publicKeyFile.exists())
340         {
341            LOGGER.warning("The public key file " + StringUtil.singleQuote(publicKeyFile.getPath()) + " does not exist!");
342            createNewKeys = true;
343         }
344         else if (! publicKeyFile.canRead())
345         {
346            LOGGER.warning("The public key file " + StringUtil.singleQuote(publicKeyFile.getPath()) + " is not readable by the user executing the app server!");
347            createNewKeys = true;
348         }
349
350         File privateKeyFile = new File(keyDir, "ECPrivate.key");
351         if (! privateKeyFile.exists())
352         {
353            LOGGER.warning("The private key file " + StringUtil.singleQuote(privateKeyFile.getPath()) + " does not exist!");
354            createNewKeys = true;
355         }
356         else if (! publicKeyFile.canRead())
357         {
358            LOGGER.warning("The private key file " + StringUtil.singleQuote(privateKeyFile.getPath()) + " is not readable by the user executing the app server!");
359            createNewKeys = true;
360         }
361
362         try
363         {
364            PublicKey publicKey = CryptoUtil.readPublicEllipticCurveKeyFile(publicKeyFile);
365            PrivateKey privateKey = CryptoUtil.readPrivateEllipticCurveKeyFile(privateKeyFile);
366
367            keyPair = new KeyPair(publicKey, privateKey);
368         }
369         catch (Exception e)
370         {
371            LOGGER.warning("Problem reading the EC key files: " + e.getMessage());
372            createNewKeys = true;
373         }
374      }
375
376      if (createNewKeys)
377      {
378         if (! keyDir.exists())
379         {
380            keyDir = new File(System.getProperty("user.dir"));
381         }
382
383         try
384         {
385            keyPair = CryptoUtil.generateEllipticCurveKeyPair();
386            CryptoUtil.writePublicEllipticCurveKeyToFile(keyPair.getPublic(), new File(keyDir, "ECPublic.key"));
387            CryptoUtil.writePrivateEllipticCurveKeyToFile(keyPair.getPrivate(), new File(keyDir, "ECPrivate.key"));
388            LOGGER.warning("New EC keys created in " + StringUtil.singleQuote(keyDir.getPath()) + ".");
389         }
390         catch (Exception e)
391         {
392            LOGGER.warning("Could not create EC keys in directory " + StringUtil.singleQuote(keyDir.getPath()) + "!");
393            keyPair = null;
394         }
395      }
396
397
398      return keyPair;
399   }
400
401   //---------------------------------------------------------------------------
402   protected boolean browserSessionCookieIsValid(HfgCookie inSessionCookie)
403      throws Exception
404   {
405      boolean result = false;
406
407      if (inSessionCookie != null
408          && mECKeyPair != null)
409      {
410         String cookieValue = getBrowserSessionCookieValue(inSessionCookie);
411         int sigIndex = cookieValue.indexOf("Sig:");
412         if (sigIndex > 0)
413         {
414            String cookieData = cookieValue.substring(0, sigIndex);
415            byte[] signature = Base64.getDecoder().decode(cookieValue.substring(sigIndex + 4));
416
417            result = CryptoUtil.verifySignatureWithECDSA(mECKeyPair.getPublic(), cookieData, signature);
418         }
419      }
420
421      return result;
422   }
423
424   //---------------------------------------------------------------------------
425   protected HfgCookie generateBrowserSessionCookie(UserImpl inUser)
426         throws Exception
427   {
428      HfgCookie cookie = null;
429
430      if (mECKeyPair != null)
431      {
432         String data = inUser.toXMLTag().toXML();
433         byte[] signature = CryptoUtil.generateSignatureWithECDSA(mECKeyPair.getPrivate(), data);
434
435         // Create the Base64-encoded cookie
436         String cookieValue = Base64.getEncoder().encodeToString((data + "Sig:" + Base64.getEncoder().encodeToString(signature)).getBytes());
437
438         cookie = new HfgCookie(BROWSER_SESSION_AUTH_COOKIE_NAME, cookieValue);
439         cookie.setPath("/");
440         cookie.setMaxAge(-1); // Expire at the end of the browser session
441      }
442
443      return cookie;
444   }
445
446   //---------------------------------------------------------------------------
447   protected static UserImpl createUserObjFromBrowserSessionCookie(HfgCookie inSessionCookie)
448         throws XMLException, IOException
449   {
450      UserImpl user = null;
451
452      String cookieValue = getBrowserSessionCookieValue(inSessionCookie);
453      int sigIndex = cookieValue.indexOf("Sig:");
454      if (sigIndex > 0)
455      {
456         String cookieData = cookieValue.substring(0, sigIndex);
457
458         XMLTag xmlTag = new XMLTag(new ByteArrayInputStream(cookieData.getBytes()));
459         user = new UserImpl(xmlTag);
460      }
461
462      return user;
463   }
464
465   //---------------------------------------------------------------------------
466   private static String getBrowserSessionCookieValue(HfgCookie inCookie)
467   {
468      String value = null;
469
470      if (inCookie != null)
471      {
472         try
473         {
474            String urlDecodedCookieValue = inCookie.getValue();
475            if(inCookie.getValue().contains("%"))
476            {
477               //percent sign indicates that the current value has been encoded.
478               urlDecodedCookieValue = URLDecoder.decode(inCookie.getValue(), "UTF-8");
479            }
480            value = new String(Base64.getDecoder().decode(urlDecodedCookieValue));
481         }
482         catch (Exception e)
483         {
484            // Backwards-compatibility with the un-Base64 version of the cookie. Just in case.
485            value = inCookie.getDecodedValue();
486         }
487      }
488
489      return value;
490   }
491
492   //---------------------------------------------------------------------------
493   protected HfgCookie generateAuthUsernameCookie(String inUsername, String inDomain)
494         throws Exception
495   {
496      HfgCookie cookie = null;
497
498      StringBuilderPlus cookieValue = new StringBuilderPlus().setDelimiter(";")
499                                                             .append(USERNAME + "=" + inUsername)
500                                                             .delimitedAppend(DOMAIN + "=" + inDomain);
501
502      cookie = new HfgCookie(AUTH_USERNAME_COOKIE_NAME, cookieValue.toString());
503      cookie.setPath("/");
504      cookie.setMaxAge(new TimeSpan(6, "months").convertTo(TimeUnit.second).intValue()); // Expire in 6 months
505
506      return cookie;
507   }
508
509   //---------------------------------------------------------------------------
510   public static HfgCookie getAuthUsernameCookie(HttpServletRequest inRequest)
511   {
512      HfgCookie authCookie = null;
513      Cookie[] cookies = inRequest.getCookies();
514      if (cookies != null
515            && cookies.length > 0)
516      {
517         for (Cookie cookie : cookies)
518         {
519            if (cookie.getName().equals(AUTH_USERNAME_COOKIE_NAME))
520            {
521               authCookie = new HfgCookie(cookie);
522               break;
523            }
524         }
525      }
526
527      return authCookie;
528   }
529
530   //---------------------------------------------------------------------------
531   protected static LoginCredentials getAuthUsernameCookieValue(HfgCookie inCookie)
532   {
533      LoginCredentials credentials = null;
534
535      if (inCookie != null)
536      {
537         String urlDecodedCookieValue = inCookie.getValue();
538         if (inCookie.getValue().contains("%"))
539         {
540            try
541            {
542               //percent sign indicates that the current value has been encoded.
543               urlDecodedCookieValue = URLDecoder.decode(inCookie.getValue(), "UTF-8");
544            }
545            catch (UnsupportedEncodingException e)
546            {
547               throw new RuntimeException(e);
548            }
549         }
550
551         credentials = new LoginCredentials(null, null);
552
553         String[] pieces = urlDecodedCookieValue.split(";");
554         for (String piece : pieces)
555         {
556            String[] fields = piece.split("=");
557            if (fields[0].equals(USERNAME))
558            {
559               credentials.setUser(fields[1]);
560            }
561            else if (fields[0].equals(DOMAIN))
562            {
563               credentials.setDomain(fields[1]);
564            }
565         }
566      }
567
568      return credentials;
569   }
570
571}