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}