001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/providers/tags/sakai_2-4-1/kerberos/src/java/org/sakaiproject/component/kerberos/user/KerberosUserDirectoryProvider.java $
003: * $Id: KerberosUserDirectoryProvider.java 17982 2006-11-06 18:04:15Z slt@columbia.edu $
004: ***********************************************************************************
005: *
006: * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: **********************************************************************************/package org.sakaiproject.component.kerberos.user;
021:
022: import java.io.File;
023: import java.io.IOException;
024: import java.security.MessageDigest;
025: import java.util.Collection;
026: import java.util.Hashtable;
027: import java.util.Iterator;
028:
029: import javax.security.auth.callback.Callback;
030: import javax.security.auth.callback.CallbackHandler;
031: import javax.security.auth.callback.ConfirmationCallback;
032: import javax.security.auth.callback.NameCallback;
033: import javax.security.auth.callback.PasswordCallback;
034: import javax.security.auth.callback.TextOutputCallback;
035: import javax.security.auth.callback.UnsupportedCallbackException;
036: import javax.security.auth.login.LoginContext;
037: import javax.security.auth.login.LoginException;
038:
039: import org.apache.commons.logging.Log;
040: import org.apache.commons.logging.LogFactory;
041: import org.sakaiproject.component.cover.ServerConfigurationService;
042: import org.sakaiproject.user.api.UserDirectoryProvider;
043: import org.sakaiproject.user.api.UserEdit;
044: import org.sakaiproject.util.StringUtil;
045:
046: import sun.misc.BASE64Encoder;
047:
048: /**
049: * <p>
050: * KerberosUserDirectoryProvider is a UserDirectoryProvider that authenticates usernames using Kerberos.
051: * </p>
052: * <p>
053: * For more information on configuration, see the README.txt file
054: * <p>
055: */
056: public class KerberosUserDirectoryProvider implements
057: UserDirectoryProvider {
058: /** Our log (commons). */
059: private static Log M_log = LogFactory
060: .getLog(KerberosUserDirectoryProvider.class);
061:
062: /**********************************************************************************************************************************************************************************************************************************************************
063: * Dependencies and their setter methods
064: *********************************************************************************************************************************************************************************************************************************************************/
065:
066: /**********************************************************************************************************************************************************************************************************************************************************
067: * Configuration options and their setter methods
068: *********************************************************************************************************************************************************************************************************************************************************/
069:
070: /** Configuration: Domain */
071: protected String m_domain = "domain.tld";
072:
073: /**
074: * Configuration: Domain Name (for E-Mail Addresses)
075: *
076: * @param domain
077: * The domain in the form of "domain.tld"
078: */
079: public void setDomain(String domain) {
080: m_domain = domain;
081: }
082:
083: /** Configuration: LoginContext */
084: protected String m_logincontext = "KerberosAuthentication";
085:
086: /**
087: * Configuration: Authentication Name
088: *
089: * @param logincontext
090: * The context to be used from the login.config file - default "KerberosAuthentication"
091: */
092: public void setLoginContext(String logincontext) {
093: m_logincontext = logincontext;
094: }
095:
096: /** Configuration: RequireLocalAccount */
097: protected boolean m_requirelocalaccount = true;
098:
099: /**
100: * Configuration: Require Local Account
101: *
102: * @param requirelocalaccount
103: * Determine if a local account is required for user to authenticate - default "true"
104: */
105: public void setRequireLocalAccount(Boolean requirelocalaccount) {
106: m_requirelocalaccount = requirelocalaccount.booleanValue();
107: }
108:
109: /** Configuration: KnownUserMsg */
110: protected String m_knownusermsg = "Integrity check on decrypted field failed";
111:
112: /**
113: * Configuration: Kerberos Error Message
114: *
115: * @param knownusermsg
116: * Start of error returned for bad logins by known users - default is from RFC 1510
117: */
118: public void setKnownUserMsg(String knownusermsg) {
119: m_knownusermsg = knownusermsg;
120: }
121:
122: /** Configuration: Cachettl */
123: protected int m_cachettl = 5 * 60 * 1000;
124:
125: /**
126: * Configuration: Cache TTL
127: *
128: * @param cachettl
129: * Time (in milliseconds) to cache authenticated usernames - default is 300000 ms (5 minutes)
130: */
131: public void setCachettl(int cachettl) {
132: m_cachettl = cachettl;
133: }
134:
135: /**
136: * Hash table for auth caching
137: */
138:
139: private Hashtable users = new Hashtable();
140:
141: /**********************************************************************************************************************************************************************************************************************************************************
142: * Init and Destroy
143: *********************************************************************************************************************************************************************************************************************************************************/
144:
145: /**
146: * Final initialization, once all dependencies are set.
147: */
148: public void init() {
149: try {
150:
151: // Full paths only from the file
152: String kerberoskrb5conf = ServerConfigurationService
153: .getString("provider.kerberos.krb5.conf", null);
154: String kerberosauthloginconfig = ServerConfigurationService
155: .getString("provider.kerberos.auth.login.config",
156: null);
157: boolean kerberosshowconfig = ServerConfigurationService
158: .getBoolean("provider.kerberos.showconfig", false);
159: String sakaihomepath = System.getProperty("sakai.home");
160:
161: // if locations are configured in sakai.properties, use them in place of the current system locations
162: // if the location specified exists and is readable, use full absolute path
163: // otherwise, try file path relative to sakai.home
164: // if files are readable use the, otherwise print warning and use system defaults
165: if (kerberoskrb5conf != null) {
166: if (new File(kerberoskrb5conf).canRead()) {
167: System.setProperty("java.security.krb5.conf",
168: kerberoskrb5conf);
169: } else if (new File(sakaihomepath + kerberoskrb5conf)
170: .canRead()) {
171: System.setProperty("java.security.krb5.conf",
172: sakaihomepath + kerberoskrb5conf);
173: } else {
174: M_log.warn(this
175: + ".init(): Cannot set krb5conf location");
176: kerberoskrb5conf = null;
177: }
178: }
179:
180: if (kerberosauthloginconfig != null) {
181:
182: if (new File(kerberosauthloginconfig).canRead()) {
183: System.setProperty(
184: "java.security.auth.login.config",
185: kerberosauthloginconfig);
186: } else if (new File(sakaihomepath
187: + kerberosauthloginconfig).canRead()) {
188: System.setProperty(
189: "java.security.auth.login.config",
190: sakaihomepath + kerberosauthloginconfig);
191: } else {
192: M_log
193: .warn(this
194: + ".init(): Cannot set kerberosauthloginconfig location");
195: kerberosauthloginconfig = null;
196: }
197: }
198:
199: M_log.info(this + ".init()" + " Domain=" + m_domain
200: + " LoginContext=" + m_logincontext
201: + " RequireLocalAccount=" + m_requirelocalaccount
202: + " KnownUserMsg=" + m_knownusermsg + " CacheTTL="
203: + m_cachettl);
204:
205: // show the whole config if set
206: // system locations will read NULL if not set (system defaults will be used)
207: if (kerberosshowconfig) {
208: M_log
209: .info(this
210: + ".init()"
211: + " SakaiHome="
212: + sakaihomepath
213: + " SakaiPropertyKrb5Conf="
214: + kerberoskrb5conf
215: + " SakaiPropertyAuthLoginConfig="
216: + kerberosauthloginconfig
217: + " SystemPropertyKrb5Conf="
218: + System
219: .getProperty("java.security.krb5.conf")
220: + " SystemPropertyAuthLoginConfig="
221: + System
222: .getProperty("java.security.auth.login.config"));
223: }
224:
225: } catch (Throwable t) {
226: M_log.warn(this + ".init(): ", t);
227: }
228:
229: } // init
230:
231: /**
232: * Returns to uninitialized state. You can use this method to release resources thet your Service allocated when Turbine shuts down.
233: */
234: public void destroy() {
235: M_log.info(this + ".destroy()");
236:
237: } // destroy
238:
239: /**********************************************************************************************************************************************************************************************************************************************************
240: * UserDirectoryProvider implementation
241: *********************************************************************************************************************************************************************************************************************************************************/
242:
243: /**
244: * See if a user by this id exists.
245: *
246: * @param userId
247: * The user id string.
248: * @return true if a user by this id exists, false if not.
249: */
250: public boolean userExists(String userId) {
251: if (m_requirelocalaccount)
252: return false;
253:
254: boolean knownKerb = userKnownToKerberos(userId);
255: M_log.info("userExists: " + userId + " Kerberos: " + knownKerb);
256: return knownKerb;
257: } // userExists
258:
259: /**
260: * Access a user object. Update the object with the information found.
261: *
262: * @param edit
263: * The user object (id is set) to fill in.
264: * @return true if the user object was found and information updated, false if not.
265: */
266: public boolean getUser(UserEdit edit) {
267: if (!userExists(edit.getEid()))
268: return false;
269:
270: edit.setEmail(edit.getEid() + "@" + m_domain);
271: edit.setType("kerberos");
272:
273: return true;
274: } // getUser
275:
276: /**
277: * Access a collection of UserEdit objects; if the user is found, update the information, otherwise remove the UserEdit object from the collection.
278: *
279: * @param users
280: * The UserEdit objects (with id set) to fill in or remove.
281: */
282: public void getUsers(Collection users) {
283: for (Iterator i = users.iterator(); i.hasNext();) {
284: UserEdit user = (UserEdit) i.next();
285: if (!getUser(user)) {
286: i.remove();
287: }
288: }
289: }
290:
291: /**
292: * Find a user object who has this email address. Update the object with the information found.
293: *
294: * @param email
295: * The email address string.
296: * @return true if the user object was found and information updated, false if not.
297: */
298: public boolean findUserByEmail(UserEdit edit, String email) {
299: // lets not get messed up with spaces or cases
300: String test = email.toLowerCase().trim();
301:
302: // if the email ends with "domain.tld" (even if it's from somebody@foo.bar.domain.tld)
303: // use the local part as a user id.
304:
305: if (!test.endsWith(m_domain))
306: return false;
307:
308: // split the string once at the first "@"
309: String parts[] = StringUtil.splitFirst(test, "@");
310: edit.setEid(parts[0]);
311: return getUser(edit);
312:
313: } // findUserByEmail
314:
315: /**
316: * Authenticate a user / password. Check for an "valid, previously authenticated" user in in-memory table.
317: *
318: * @param id
319: * The user id.
320: * @param edit
321: * The UserEdit matching the id to be authenticated (and updated) if we have one.
322: * @param password
323: * The password.
324: * @return true if authenticated, false if not.
325: */
326: public boolean authenticateUser(String userId, UserEdit edit,
327: String password) {
328: // The in-memory caching mechanism is implemented here
329: // try to get user from in-memory hashtable
330: try {
331: UserData existingUser = (UserData) users.get(userId);
332:
333: boolean authUser = false;
334: String hpassword = encodeSHA(password);
335:
336: // Check for user in in-memory hashtable. To be a "valid, previously authenticated" user,
337: // 3 conditions must be met:
338: //
339: // 1) an entry for the userId must exist in the cache
340: // 2) the last usccessful authentication was < cachettl milliseconds ago
341: // 3) the one-way hash of the entered password must be equivalent to what is stored in the cache
342: //
343: // If these conditions are not, the authentication is performed via JAAS and, if sucessful, a new entry is created
344:
345: if (existingUser == null
346: || (System.currentTimeMillis() - existingUser
347: .getTimeStamp()) > m_cachettl
348: || !(existingUser.getHpw().equals(hpassword))) {
349: if (M_log.isDebugEnabled())
350: M_log.debug("authenticateUser(): user " + userId
351: + " not in table, querying Kerberos");
352:
353: boolean authKerb = authenticateKerberos(userId,
354: password);
355:
356: // if authentication succeeds, create entry for authenticated user in cache;
357: // otherwise, remove any entries for this user from cache
358:
359: if (authKerb) {
360: if (M_log.isDebugEnabled())
361: M_log
362: .debug("authenticateUser(): putting authenticated user ("
363: + userId
364: + ") in table for caching");
365:
366: UserData u = new UserData(); // create entry for authenticated user in cache
367: u.setId(userId);
368: u.setHpw(hpassword);
369: u.setTimeStamp(System.currentTimeMillis());
370: users.put(userId, u); // put entry for authenticated user into cache
371:
372: } else {
373: users.remove(userId);
374: }
375:
376: authUser = authKerb;
377:
378: } else {
379: if (M_log.isDebugEnabled())
380: M_log
381: .debug("authenticateUser(): found authenticated user ("
382: + existingUser.getId()
383: + ") in table");
384: authUser = true;
385: }
386:
387: return authUser;
388: } catch (Exception e) {
389: if (M_log.isDebugEnabled())
390: M_log.debug("authenticateUser(): exception: " + e);
391: return false;
392: }
393: } // authenticateUser
394:
395: /**
396: * {@inheritDoc}
397: */
398: public void destroyAuthentication() {
399:
400: }
401:
402: /**
403: * Will this provider update user records on successful authentication? If so, the UserDirectoryService will cause these updates to be stored.
404: *
405: * @return true if the user record may be updated after successful authentication, false if not.
406: */
407: public boolean updateUserAfterAuthentication() {
408: return false;
409: }
410:
411: /**********************************************************************************************************************************************************************************************************************************************************
412: * Kerberos stuff
413: *********************************************************************************************************************************************************************************************************************************************************/
414:
415: /**
416: * Authenticate the user id and pw with Kerberos.
417: *
418: * @param user
419: * The user id.
420: * @param password
421: * the user supplied password.
422: * @return true if successful, false if not.
423: */
424: protected boolean authenticateKerberos(String user, String pw) {
425: // assure some length to the password
426: if ((pw == null) || (pw.length() == 0))
427: return false;
428:
429: // Obtain a LoginContext, needed for authentication. Tell it
430: // to use the LoginModule implementation specified by the
431: // appropriate entry in the JAAS login configuration
432: // file and to also use the specified CallbackHandler.
433: LoginContext lc = null;
434: try {
435: SakaiCallbackHandler t = new SakaiCallbackHandler();
436: t.setId(user);
437: t.setPw(pw);
438: lc = new LoginContext(m_logincontext, t);
439: } catch (LoginException le) {
440: if (M_log.isDebugEnabled())
441: M_log.debug("authenticateKerberos(): " + le.toString());
442: return false;
443: } catch (SecurityException se) {
444: if (M_log.isDebugEnabled())
445: M_log.debug("authenticateKerberos(): " + se.toString());
446: return false;
447: }
448:
449: try {
450: // attempt authentication
451: lc.login();
452: lc.logout();
453:
454: if (M_log.isDebugEnabled())
455: M_log.debug("authenticateKerberos(" + user
456: + ", pw): Kerberos auth success");
457:
458: return true;
459: } catch (LoginException le) {
460: if (M_log.isDebugEnabled())
461: M_log.debug("authenticateKerberos(" + user
462: + ", pw): Kerberos auth failed: "
463: + le.toString());
464:
465: return false;
466: }
467:
468: } // authenticateKerberos
469:
470: /**
471: * Check if the user id is known to kerberos.
472: *
473: * @param user
474: * The user id.
475: * @return true if successful, false if not.
476: */
477: private boolean userKnownToKerberos(String user) {
478: // use a dummy password
479: String pw = "dummy";
480:
481: // Obtain a LoginContext, needed for authentication.
482: // Tell it to use the LoginModule implementation specified
483: // in the JAAS login configuration file and to use
484: // use the specified CallbackHandler.
485: LoginContext lc = null;
486: try {
487: SakaiCallbackHandler t = new SakaiCallbackHandler();
488: t.setId(user);
489: t.setPw(pw);
490: lc = new LoginContext(m_logincontext, t);
491: } catch (LoginException le) {
492: if (M_log.isDebugEnabled())
493: M_log.debug("useKnownToKerberos(): " + le.toString());
494: return false;
495: } catch (SecurityException se) {
496: if (M_log.isDebugEnabled())
497: M_log.debug("useKnownToKerberos(): " + se.toString());
498: return false;
499: }
500:
501: try {
502: // attempt authentication
503: lc.login();
504: lc.logout();
505:
506: if (M_log.isDebugEnabled())
507: M_log.debug("useKnownToKerberos(" + user
508: + "): Kerberos auth success");
509:
510: return true;
511: } catch (LoginException le) {
512: String msg = le.getMessage();
513:
514: // if this is the message, the user was good, the password was bad
515: if (msg.startsWith(m_knownusermsg)) {
516: if (M_log.isDebugEnabled())
517: M_log.debug("userKnownToKerberos(" + user
518: + "): Kerberos user known (bad pw)");
519:
520: return true;
521: }
522:
523: // the other message is when the user is bad:
524: if (M_log.isDebugEnabled())
525: M_log.debug("userKnownToKerberos(" + user
526: + "): Kerberos user unknown or invalid");
527:
528: return false;
529: }
530:
531: } // userKnownToKerberos
532:
533: /**
534: * Inner Class SakaiCallbackHandler Get the user id and password information for authentication purpose. This can be used by a JAAS application to instantiate a CallbackHandler.
535: *
536: * @see javax.security.auth.callback
537: */
538: protected class SakaiCallbackHandler implements CallbackHandler {
539: private String m_id;
540:
541: private String m_pw;
542:
543: /** constructor */
544: public SakaiCallbackHandler() {
545: m_id = new String("");
546: m_pw = new String("");
547:
548: } // SakaiCallbackHandler
549:
550: /**
551: * Handles the specified set of callbacks.
552: *
553: * @param callbacks
554: * the callbacks to handle
555: * @throws IOException
556: * if an input or output error occurs.
557: * @throws UnsupportedCallbackException
558: * if the callback is not an instance of NameCallback or PasswordCallback
559: */
560: public void handle(Callback[] callbacks)
561: throws java.io.IOException,
562: UnsupportedCallbackException {
563: ConfirmationCallback confirmation = null;
564:
565: for (int i = 0; i < callbacks.length; i++) {
566: if (callbacks[i] instanceof TextOutputCallback) {
567: if (M_log.isDebugEnabled())
568: M_log
569: .debug("SakaiCallbackHandler: TextOutputCallback");
570: }
571:
572: else if (callbacks[i] instanceof NameCallback) {
573: NameCallback nc = (NameCallback) callbacks[i];
574:
575: String result = getId();
576: if (result.equals("")) {
577: result = nc.getDefaultName();
578: }
579:
580: nc.setName(result);
581: }
582:
583: else if (callbacks[i] instanceof PasswordCallback) {
584: PasswordCallback pc = (PasswordCallback) callbacks[i];
585: pc.setPassword(getPw());
586: }
587:
588: else if (callbacks[i] instanceof ConfirmationCallback) {
589: if (M_log.isDebugEnabled())
590: M_log
591: .debug("SakaiCallbackHandler: ConfirmationCallback");
592: }
593:
594: else {
595: throw new UnsupportedCallbackException(
596: callbacks[i],
597: "SakaiCallbackHandler: Unrecognized Callback");
598: }
599: }
600:
601: } // handle
602:
603: void setId(String id) {
604: m_id = id;
605:
606: } // setId
607:
608: private String getId() {
609: return m_id;
610:
611: } // getid
612:
613: void setPw(String pw) {
614: m_pw = pw;
615:
616: } // setPw
617:
618: private char[] getPw() {
619: return m_pw.toCharArray();
620:
621: } // getPw
622:
623: } // SakaiCallbackHandler
624:
625: /**
626: * {@inheritDoc}
627: */
628: public boolean authenticateWithProviderFirst(String id) {
629: return false;
630: }
631:
632: /**
633: * {@inheritDoc}
634: */
635: public boolean createUserRecord(String id) {
636: return false;
637: }
638:
639: /**
640: * <p>
641: * Helper class for storing user data in an in-memory cache
642: * </p>
643: */
644: class UserData {
645:
646: String id;
647:
648: String hpw;
649:
650: long timeStamp;
651:
652: /**
653: * @return Returns the id.
654: */
655: public String getId() {
656: return id;
657: }
658:
659: /**
660: * @param id
661: * The id to set.
662: */
663: public void setId(String id) {
664: this .id = id;
665: }
666:
667: /**
668: * @param hpw
669: * hashed pw to put in.
670: */
671: public void setHpw(String hpw) {
672: this .hpw = hpw;
673: }
674:
675: /**
676: * @return Returns the hashed password.
677: */
678:
679: public String getHpw() {
680: return hpw;
681: }
682:
683: /**
684: * @return Returns the timeStamp.
685: */
686: public long getTimeStamp() {
687: return timeStamp;
688: }
689:
690: /**
691: * @param timeStamp
692: * The timeStamp to set.
693: */
694: public void setTimeStamp(long timeStamp) {
695: this .timeStamp = timeStamp;
696: }
697:
698: } // UserData class
699:
700: /**
701: * <p>
702: * Hash string for storage in a cache using SHA
703: * </p>
704: *
705: * @param UTF-8
706: * string
707: * @return encoded hash of string
708: */
709:
710: private synchronized String encodeSHA(String plaintext) {
711:
712: try {
713: MessageDigest md = MessageDigest.getInstance("SHA");
714: md.update(plaintext.getBytes("UTF-8"));
715: byte raw[] = md.digest();
716: String hash = (new BASE64Encoder()).encode(raw);
717: return hash;
718: } catch (Exception e) {
719: M_log.warn("encodeSHA(): exception: " + e);
720: return null;
721: }
722: } // encodeSHA
723:
724: } // KerberosUserDirectoryProvider
|