001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.server.security;
031:
032: import com.caucho.security.BasicPrincipal;
033: import com.caucho.server.session.SessionImpl;
034: import com.caucho.server.session.SessionManager;
035: import com.caucho.server.webapp.Application;
036: import com.caucho.util.Alarm;
037: import com.caucho.util.L10N;
038: import com.caucho.util.LruCache;
039: import com.caucho.webbeans.component.*;
040:
041: import javax.annotation.PostConstruct;
042: import javax.servlet.ServletContext;
043: import javax.servlet.ServletException;
044: import javax.servlet.http.HttpServletRequest;
045: import javax.servlet.http.HttpServletResponse;
046: import javax.servlet.http.HttpSession;
047: import java.lang.ref.SoftReference;
048: import java.security.MessageDigest;
049: import java.security.Principal;
050: import java.util.ArrayList;
051: import java.util.logging.Level;
052: import java.util.logging.Logger;
053:
054: /**
055: * All applications should extend AbstractAuthenticator to implement
056: * their custom authenticators. While this isn't absolutely required,
057: * it protects implementations from API changes.
058: *
059: * <p>The AbstractAuthenticator provides a single-signon cache. Users
060: * logged into one web-app will share the same principal.
061: */
062: public class AbstractAuthenticator implements ServletAuthenticator,
063: HandleAware, java.io.Serializable {
064: private static final Logger log = Logger
065: .getLogger(AbstractAuthenticator.class.getName());
066: static final L10N L = new L10N(AbstractAuthenticator.class);
067:
068: public static final String LOGIN_NAME = "com.caucho.servlet.login.name";
069:
070: protected int _principalCacheSize = 4096;
071: protected LruCache<String, PrincipalEntry> _principalCache;
072:
073: protected String _passwordDigestAlgorithm = "MD5-base64";
074: protected String _passwordDigestRealm = "resin";
075: protected PasswordDigest _passwordDigest;
076:
077: private boolean _logoutOnTimeout = true;
078:
079: private Object _serializationHandle;
080:
081: /**
082: * Returns the size of the principal cache.
083: */
084: public int getPrincipalCacheSize() {
085: return _principalCacheSize;
086: }
087:
088: /**
089: * Sets the size of the principal cache.
090: */
091: public void setPrincipalCacheSize(int size) {
092: _principalCacheSize = size;
093: }
094:
095: /**
096: * Returns the password digest
097: */
098: public PasswordDigest getPasswordDigest() {
099: return _passwordDigest;
100: }
101:
102: /**
103: * Sets the password digest. The password digest of the form:
104: * "algorithm-format", e.g. "MD5-base64".
105: */
106: public void setPasswordDigest(PasswordDigest digest) {
107: _passwordDigest = digest;
108: }
109:
110: /**
111: * Returns the password digest algorithm
112: */
113: public String getPasswordDigestAlgorithm() {
114: return _passwordDigestAlgorithm;
115: }
116:
117: /**
118: * Sets the password digest algorithm. The password digest of the form:
119: * "algorithm-format", e.g. "MD5-base64".
120: */
121: public void setPasswordDigestAlgorithm(String digest) {
122: _passwordDigestAlgorithm = digest;
123: }
124:
125: /**
126: * Returns the password digest realm
127: */
128: public String getPasswordDigestRealm() {
129: return _passwordDigestRealm;
130: }
131:
132: /**
133: * Sets the password digest realm.
134: */
135: public void setPasswordDigestRealm(String realm) {
136: _passwordDigestRealm = realm;
137: }
138:
139: /**
140: * Returns true if the user should be logged out on a session timeout.
141: */
142: public boolean getLogoutOnSessionTimeout() {
143: return _logoutOnTimeout;
144: }
145:
146: /**
147: * Sets true if the principal should logout when the session times out.
148: */
149: public void setLogoutOnSessionTimeout(boolean logout) {
150: _logoutOnTimeout = logout;
151: }
152:
153: /**
154: * Adds a role mapping.
155: */
156: public void addRoleMapping(Principal principal, String role) {
157: }
158:
159: /**
160: * Initialize the authenticator with the application.
161: */
162: @PostConstruct
163: public void init() throws ServletException {
164: if (_principalCacheSize > 0)
165: _principalCache = new LruCache<String, PrincipalEntry>(
166: _principalCacheSize);
167:
168: if (_passwordDigest != null) {
169: if (_passwordDigest.getAlgorithm() == null
170: || _passwordDigest.getAlgorithm().equals("none"))
171: _passwordDigest = null;
172: } else if (_passwordDigestAlgorithm == null
173: || _passwordDigestAlgorithm.equals("none")) {
174: } else {
175: int p = _passwordDigestAlgorithm.indexOf('-');
176:
177: if (p > 0) {
178: String algorithm = _passwordDigestAlgorithm.substring(
179: 0, p);
180: String format = _passwordDigestAlgorithm
181: .substring(p + 1);
182:
183: _passwordDigest = new PasswordDigest();
184: _passwordDigest.setAlgorithm(algorithm);
185: _passwordDigest.setFormat(format);
186: _passwordDigest.setRealm(_passwordDigestRealm);
187:
188: _passwordDigest.init();
189: }
190: }
191: }
192:
193: /**
194: * Logs the user in with any appropriate password.
195: */
196: public Principal login(HttpServletRequest request,
197: HttpServletResponse response, ServletContext app,
198: String user, String password) throws ServletException {
199: String digestPassword = getPasswordDigest(request, response,
200: app, user, password);
201: Principal principal = loginImpl(request, response, app, user,
202: digestPassword);
203:
204: if (principal != null) {
205: SessionImpl session = (SessionImpl) request.getSession();
206: session.setUser(principal);
207:
208: if (_principalCache != null) {
209: PrincipalEntry entry = new PrincipalEntry(principal);
210: entry.addSession(session);
211:
212: _principalCache.put(session.getId(), entry);
213: }
214: }
215:
216: return principal;
217: }
218:
219: /**
220: * Returns the digest view of the password. The default
221: * uses the PasswordDigest class if available, and returns the
222: * plaintext password if not.
223: */
224: public String getPasswordDigest(HttpServletRequest request,
225: HttpServletResponse response, ServletContext app,
226: String user, String password) throws ServletException {
227:
228: if (_passwordDigest != null)
229: return _passwordDigest.getPasswordDigest(request, response,
230: app, user, password);
231: else
232: return password;
233: }
234:
235: /**
236: * Authenticate (login) the user.
237: */
238: protected Principal loginImpl(HttpServletRequest request,
239: HttpServletResponse response, ServletContext application,
240: String user, String password) throws ServletException {
241: return null;
242: }
243:
244: /**
245: * Validates the user when using HTTP Digest authentication.
246: * DigestLogin will call this method. Most other AbstractLogin
247: * implementations, like BasicLogin and FormLogin, will use
248: * getUserPrincipal instead.
249: *
250: * <p>The HTTP Digest authentication uses the following algorithm
251: * to calculate the digest. The digest is then compared to
252: * the client digest.
253: *
254: * <code><pre>
255: * A1 = MD5(username + ':' + realm + ':' + password)
256: * A2 = MD5(method + ':' + uri)
257: * digest = MD5(A1 + ':' + nonce + A2)
258: * </pre></code>
259: *
260: * @param request the request trying to authenticate.
261: * @param response the response for setting headers and cookies.
262: * @param app the servlet context
263: * @param user the username
264: * @param realm the authentication realm
265: * @param nonce the nonce passed to the client during the challenge
266: * @param uri te protected uri
267: * @param qop
268: * @param nc
269: * @param cnonce the client nonce
270: * @param clientDigest the client's calculation of the digest
271: *
272: * @return the logged in principal if successful
273: */
274: public Principal loginDigest(HttpServletRequest request,
275: HttpServletResponse response, ServletContext app,
276: String user, String realm, String nonce, String uri,
277: String qop, String nc, String cnonce, byte[] clientDigest)
278: throws ServletException {
279: Principal principal = loginDigestImpl(request, response, app,
280: user, realm, nonce, uri, qop, nc, cnonce, clientDigest);
281:
282: if (principal != null) {
283: SessionImpl session = (SessionImpl) request.getSession();
284: session.setUser(principal);
285:
286: if (_principalCache != null) {
287: PrincipalEntry entry = new PrincipalEntry(principal);
288: entry.addSession(session);
289:
290: _principalCache.put(session.getId(), entry);
291: }
292: }
293:
294: return principal;
295: }
296:
297: /**
298: * Validates the user when HTTP Digest authentication.
299: * The HTTP Digest authentication uses the following algorithm
300: * to calculate the digest. The digest is then compared to
301: * the client digest.
302: *
303: * <code><pre>
304: * A1 = MD5(username + ':' + realm + ':' + password)
305: * A2 = MD5(method + ':' + uri)
306: * digest = MD5(A1 + ':' + nonce + A2)
307: * </pre></code>
308: *
309: * @param request the request trying to authenticate.
310: * @param response the response for setting headers and cookies.
311: * @param app the servlet context
312: * @param user the username
313: * @param realm the authentication realm
314: * @param nonce the nonce passed to the client during the challenge
315: * @param uri te protected uri
316: * @param qop
317: * @param nc
318: * @param cnonce the client nonce
319: * @param clientDigest the client's calculation of the digest
320: *
321: * @return the logged in principal if successful
322: */
323: public Principal loginDigestImpl(HttpServletRequest request,
324: HttpServletResponse response, ServletContext app,
325: String user, String realm, String nonce, String uri,
326: String qop, String nc, String cnonce, byte[] clientDigest)
327: throws ServletException {
328:
329: try {
330: if (clientDigest == null)
331: return null;
332:
333: MessageDigest digest = MessageDigest.getInstance("MD5");
334:
335: byte[] a1 = getDigestSecret(request, response, app, user,
336: realm, "MD5");
337:
338: if (a1 == null)
339: return null;
340:
341: digestUpdateHex(digest, a1);
342:
343: digest.update((byte) ':');
344: for (int i = 0; i < nonce.length(); i++)
345: digest.update((byte) nonce.charAt(i));
346:
347: if (qop != null) {
348: digest.update((byte) ':');
349: for (int i = 0; i < nc.length(); i++)
350: digest.update((byte) nc.charAt(i));
351:
352: digest.update((byte) ':');
353:
354: for (int i = 0; cnonce != null && i < cnonce.length(); i++)
355: digest.update((byte) cnonce.charAt(i));
356:
357: digest.update((byte) ':');
358: for (int i = 0; qop != null && i < qop.length(); i++)
359: digest.update((byte) qop.charAt(i));
360: }
361: digest.update((byte) ':');
362:
363: byte[] a2 = digest(request.getMethod() + ":" + uri);
364:
365: digestUpdateHex(digest, a2);
366:
367: byte[] serverDigest = digest.digest();
368:
369: if (clientDigest.length != serverDigest.length)
370: return null;
371:
372: for (int i = 0; i < clientDigest.length; i++) {
373: if (serverDigest[i] != clientDigest[i])
374: return null;
375: }
376:
377: return new BasicPrincipal(user);
378: } catch (Exception e) {
379: throw new ServletException(e);
380: }
381: }
382:
383: private void digestUpdateHex(MessageDigest digest, byte[] bytes) {
384: for (int i = 0; i < bytes.length; i++) {
385: int b = bytes[i];
386: int d1 = (b >> 4) & 0xf;
387: int d2 = b & 0xf;
388:
389: if (d1 < 10)
390: digest.update((byte) (d1 + '0'));
391: else
392: digest.update((byte) (d1 + 'a' - 10));
393:
394: if (d2 < 10)
395: digest.update((byte) (d2 + '0'));
396: else
397: digest.update((byte) (d2 + 'a' - 10));
398: }
399: }
400:
401: protected byte[] stringToDigest(String digest) {
402: if (digest == null)
403: return null;
404:
405: int len = (digest.length() + 1) / 2;
406: byte[] clientDigest = new byte[len];
407:
408: for (int i = 0; i + 1 < digest.length(); i += 2) {
409: int ch1 = digest.charAt(i);
410: int ch2 = digest.charAt(i + 1);
411:
412: int b = 0;
413: if (ch1 >= '0' && ch1 <= '9')
414: b += ch1 - '0';
415: else if (ch1 >= 'a' && ch1 <= 'f')
416: b += ch1 - 'a' + 10;
417:
418: b *= 16;
419:
420: if (ch2 >= '0' && ch2 <= '9')
421: b += ch2 - '0';
422: else if (ch2 >= 'a' && ch2 <= 'f')
423: b += ch2 - 'a' + 10;
424:
425: clientDigest[i / 2] = (byte) b;
426: }
427:
428: return clientDigest;
429: }
430:
431: /**
432: * Returns the digest secret for Digest authentication.
433: */
434: protected byte[] getDigestSecret(HttpServletRequest request,
435: HttpServletResponse response, ServletContext application,
436: String username, String realm, String algorithm)
437: throws ServletException {
438: String password = getDigestPassword(request, response,
439: application, username, realm);
440:
441: if (password == null)
442: return null;
443:
444: if (_passwordDigest != null)
445: return _passwordDigest.stringToDigest(password);
446:
447: try {
448: MessageDigest digest = MessageDigest.getInstance(algorithm);
449:
450: String string = username + ":" + realm + ":" + password;
451: byte[] data = string.getBytes("UTF8");
452: return digest.digest(data);
453: } catch (Exception e) {
454: throw new ServletException(e);
455: }
456: }
457:
458: protected byte[] digest(String value) throws ServletException {
459: try {
460: MessageDigest digest = MessageDigest.getInstance("MD5");
461:
462: byte[] data = value.getBytes("UTF8");
463: return digest.digest(data);
464: } catch (Exception e) {
465: throw new ServletException(e);
466: }
467: }
468:
469: /**
470: * Returns the password for authenticators too lazy to calculate the
471: * digest.
472: */
473: protected String getDigestPassword(HttpServletRequest request,
474: HttpServletResponse response, ServletContext application,
475: String username, String realm) throws ServletException {
476: return null;
477: }
478:
479: /**
480: * Grab the user from the request, assuming the user has
481: * already logged in. In other words, overriding methods could
482: * use cookies or the session to find the logged in principal, but
483: * shouldn't try to log the user in with form parameters.
484: *
485: * @param request the servlet request.
486: *
487: * @return a Principal representing the user or null if none has logged in.
488: */
489: public Principal getUserPrincipal(HttpServletRequest request,
490: HttpServletResponse response, ServletContext application)
491: throws ServletException {
492: SessionImpl session = (SessionImpl) request.getSession(false);
493: Principal user = null;
494:
495: if (session != null)
496: user = session.getUser();
497:
498: if (user != null)
499: return user;
500:
501: PrincipalEntry entry = null;
502:
503: if (_principalCache == null) {
504: } else if (session != null)
505: entry = _principalCache.get(session.getId());
506: else if (request.getRequestedSessionId() != null)
507: entry = _principalCache
508: .get(request.getRequestedSessionId());
509:
510: if (entry != null) {
511: user = entry.getPrincipal();
512:
513: if (session == null)
514: session = (SessionImpl) request.getSession(true);
515:
516: session.setUser(user);
517: entry.addSession(session);
518:
519: return user;
520: }
521:
522: user = getUserPrincipalImpl(request, application);
523:
524: if (user == null) {
525: } else if (session != null) {
526: entry = new PrincipalEntry(user);
527:
528: session.setUser(user);
529: entry.addSession(session);
530:
531: _principalCache.put(session.getId(), entry);
532: } else if (request.getRequestedSessionId() != null) {
533: entry = new PrincipalEntry(user);
534:
535: _principalCache.put(request.getRequestedSessionId(), entry);
536: }
537:
538: return user;
539: }
540:
541: /**
542: * Gets the user from a persistent cookie, uaing authenticateCookie
543: * to actually look the cookie up.
544: */
545: protected Principal getUserPrincipalImpl(
546: HttpServletRequest request, ServletContext application)
547: throws ServletException {
548: return null;
549: }
550:
551: /**
552: * Returns true if the user plays the named role.
553: *
554: * @param request the servlet request
555: * @param user the user to test
556: * @param role the role to test
557: */
558: public boolean isUserInRole(HttpServletRequest request,
559: HttpServletResponse response, ServletContext application,
560: Principal user, String role) throws ServletException {
561: return false;
562: }
563:
564: /**
565: * Logs the user out from the session.
566: *
567: * @param application the application
568: * @param timeoutSession the session timing out, null if not a timeout logout
569: * @param user the logged in user
570: */
571: public void logout(ServletContext application,
572: HttpSession timeoutSession, String sessionId, Principal user)
573: throws ServletException {
574: if (log.isLoggable(Level.FINE))
575: log.fine(this + " logout " + user);
576:
577: if (sessionId != null) {
578: if (_principalCache == null) {
579: } else if (timeoutSession != null) {
580: PrincipalEntry entry = _principalCache.get(sessionId);
581:
582: if (entry != null && entry.logout(timeoutSession)) {
583: _principalCache.remove(sessionId);
584: }
585: } else {
586: PrincipalEntry entry = _principalCache
587: .remove(sessionId);
588:
589: if (entry != null)
590: entry.logout();
591: }
592:
593: Application app = (Application) application;
594: SessionManager manager = app.getSessionManager();
595:
596: if (manager != null) {
597: try {
598: SessionImpl session = manager.getSession(sessionId,
599: Alarm.getCurrentTime(), false, true);
600:
601: if (session != null) {
602: session.finish();
603: session.logout();
604: }
605: } catch (Exception e) {
606: log.log(Level.FINE, e.toString(), e);
607: }
608: }
609: }
610: }
611:
612: /**
613: * Logs the user out from the session.
614: *
615: * @param request the servlet request
616: * @deprecated
617: */
618: public void logout(HttpServletRequest request,
619: HttpServletResponse response, ServletContext application,
620: Principal user) throws ServletException {
621: logout(application, null, request.getRequestedSessionId(), user);
622: }
623:
624: /**
625: * Logs the user out from the session.
626: *
627: * @param request the servlet request
628: * @deprecated
629: */
630: public void logout(ServletContext application, String sessionId,
631: Principal user) throws ServletException {
632: logout(application, null, sessionId, user);
633: }
634:
635: static class PrincipalEntry {
636: private Principal _principal;
637: private ArrayList<SoftReference<SessionImpl>> _sessions;
638:
639: PrincipalEntry(Principal principal) {
640: _principal = principal;
641: }
642:
643: Principal getPrincipal() {
644: return _principal;
645: }
646:
647: void addSession(SessionImpl session) {
648: if (_sessions == null)
649: _sessions = new ArrayList<SoftReference<SessionImpl>>();
650:
651: _sessions.add(new SoftReference<SessionImpl>(session));
652: }
653:
654: /**
655: * Logout only the given session, returning true if it's the
656: * last session to logout.
657: */
658: boolean logout(HttpSession timeoutSession) {
659: ArrayList<SoftReference<SessionImpl>> sessions = _sessions;
660:
661: if (sessions == null)
662: return true;
663:
664: boolean isEmpty = true;
665: for (int i = sessions.size() - 1; i >= 0; i--) {
666: SoftReference<SessionImpl> ref = sessions.get(i);
667: SessionImpl session = ref.get();
668:
669: try {
670: if (session == timeoutSession) {
671: sessions.remove(i);
672: session.logout();
673: } else if (session == null)
674: sessions.remove(i);
675: else
676: isEmpty = false;
677: } catch (Exception e) {
678: log.log(Level.WARNING, e.toString(), e);
679: }
680: }
681:
682: return isEmpty;
683: }
684:
685: void logout() {
686: ArrayList<SoftReference<SessionImpl>> sessions = _sessions;
687: _sessions = null;
688:
689: for (int i = 0; sessions != null && i < sessions.size(); i++) {
690: SoftReference<SessionImpl> ref = sessions.get(i);
691: SessionImpl session = ref.get();
692:
693: try {
694: if (session != null) {
695: session.logout();
696: session.invalidateLogout(); // #599, server/12i3
697: }
698: } catch (Exception e) {
699: log.log(Level.WARNING, e.toString(), e);
700: }
701: }
702: }
703: }
704:
705: /**
706: * Sets the serialization handle
707: */
708: public void setSerializationHandle(Object handle) {
709: _serializationHandle = handle;
710: }
711:
712: /**
713: * Serialize to the handle
714: */
715: public Object writeReplace() {
716: return _serializationHandle;
717: }
718:
719: public String toString() {
720: return (getClass().getSimpleName() + "["
721: + _passwordDigestAlgorithm + "," + _passwordDigestRealm + "]");
722: }
723: }
|