* KerberosUserDirectoryProvider is a UserDirectoryProvider that authenticates usernames using Kerberos. @@ -69,7 +65,7 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider *********************************************************************************************************************************************************************************************************************************************************/ /** Configuration: Domain */ - protected String m_domain = "domain.tld"; + protected String m_domain = null; /** * Configuration: Domain Name (for E-Mail Addresses) @@ -95,6 +91,33 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider { m_logincontext = logincontext; } + + /** Configuration: ServiceLoginContext */ + protected String m_servicelogincontext = "ServiceKerberosAuthentication"; + + /** + * Configuration: Service Authentication Name + * + * @param serviceLoginContext + * The context for the service to be used from the login.config file - default "ServiceKerberosAuthentication" + */ + public void setServiceLoginContext(String serviceLoginContext) + { + m_servicelogincontext = serviceLoginContext; + } + + /** Configuration: */ + protected String m_serviceprincipal; + + /** + * Configuration: GSS-API Service Principal + * + * @param serviceprincipal + * The name of the service principal for GSS-API. Needs to be set. + */ + public void setServicePrincipal(String serviceprincipal) { + this.m_serviceprincipal = serviceprincipal; + } /** Configuration: RequireLocalAccount */ protected boolean m_requirelocalaccount = true; @@ -124,26 +147,6 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider m_knownusermsg = knownusermsg; } - /** Configuration: Cachettl */ - protected int m_cachettl = 5 * 60 * 1000; - - /** - * Configuration: Cache TTL - * - * @param cachettl - * Time (in milliseconds) to cache authenticated usernames - default is 300000 ms (5 minutes) - */ - public void setCachettl(int cachettl) - { - m_cachettl = cachettl; - } - - /** - * Hash table for auth caching - */ - - private Hashtable users = new Hashtable(); - /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ @@ -153,73 +156,74 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider */ public void init() { - try + // Full paths only from the file + String kerberoskrb5conf = ServerConfigurationService.getString("provider.kerberos.krb5.conf", null); + String kerberosauthloginconfig = ServerConfigurationService.getString("provider.kerberos.auth.login.config", "sakai-jaas.conf"); + boolean kerberosshowconfig = ServerConfigurationService.getBoolean("provider.kerberos.showconfig", false); + String sakaihomepath = System.getProperty("sakai.home"); + + // if locations are configured in sakai.properties, use them in place of the current system locations + // if the location specified exists and is readable, use full absolute path + // otherwise, try file path relative to sakai.home + // if files are readable use the, otherwise print warning and use system defaults + if (kerberoskrb5conf != null) { - - // Full paths only from the file - String kerberoskrb5conf = ServerConfigurationService.getString("provider.kerberos.krb5.conf", null); - String kerberosauthloginconfig = ServerConfigurationService.getString("provider.kerberos.auth.login.config", null); - boolean kerberosshowconfig = ServerConfigurationService.getBoolean("provider.kerberos.showconfig", false); - String sakaihomepath = System.getProperty("sakai.home"); - - // if locations are configured in sakai.properties, use them in place of the current system locations - // if the location specified exists and is readable, use full absolute path - // otherwise, try file path relative to sakai.home - // if files are readable use the, otherwise print warning and use system defaults - if (kerberoskrb5conf != null) + if (new File(kerberoskrb5conf).canRead()) { - if (new File(kerberoskrb5conf).canRead()) - { - System.setProperty("java.security.krb5.conf", kerberoskrb5conf); - } - else if (new File(sakaihomepath + kerberoskrb5conf).canRead()) - { - System.setProperty("java.security.krb5.conf", sakaihomepath + kerberoskrb5conf); - } - else - { - M_log.warn(this + ".init(): Cannot set krb5conf location"); - kerberoskrb5conf = null; - } + System.setProperty("java.security.krb5.conf", kerberoskrb5conf); } - - if (kerberosauthloginconfig != null) + else if (new File(sakaihomepath, kerberoskrb5conf).canRead()) { - - if (new File(kerberosauthloginconfig).canRead()) - { - System.setProperty("java.security.auth.login.config", kerberosauthloginconfig); - } - else if (new File(sakaihomepath + kerberosauthloginconfig).canRead()) - { - System.setProperty("java.security.auth.login.config", sakaihomepath + kerberosauthloginconfig); - } - else - { - M_log.warn(this + ".init(): Cannot set kerberosauthloginconfig location"); - kerberosauthloginconfig = null; - } + System.setProperty("java.security.krb5.conf", sakaihomepath + kerberoskrb5conf); + } + else + { + M_log.info(this + ".init(): Using default rules for krb5.conf location."); + kerberoskrb5conf = null; } + } - M_log.info(this + ".init()" + " Domain=" + m_domain + " LoginContext=" + m_logincontext + " RequireLocalAccount=" - + m_requirelocalaccount + " KnownUserMsg=" + m_knownusermsg + " CacheTTL=" + m_cachettl); + if (kerberosauthloginconfig != null) + { - // show the whole config if set - // system locations will read NULL if not set (system defaults will be used) - if (kerberosshowconfig) + if (new File(kerberosauthloginconfig).canRead()) + { + System.setProperty("java.security.auth.login.config", kerberosauthloginconfig); + } + else if (new File(sakaihomepath, kerberosauthloginconfig).canRead()) { - M_log.info(this + ".init()" + " SakaiHome=" + sakaihomepath + " SakaiPropertyKrb5Conf=" + kerberoskrb5conf - + " SakaiPropertyAuthLoginConfig=" + kerberosauthloginconfig + " SystemPropertyKrb5Conf=" - + System.getProperty("java.security.krb5.conf") + " SystemPropertyAuthLoginConfig=" - + System.getProperty("java.security.auth.login.config")); + System.setProperty("java.security.auth.login.config", sakaihomepath + kerberosauthloginconfig); } + else + { + M_log.info(this + ".init(): Cannot set kerberosauthloginconfig location"); + kerberosauthloginconfig = null; + } + } + + if (m_serviceprincipal == null) + { + throw new IllegalStateException("Service principal can't be null."); + } + + M_log.info(this + ".init()" + " Domain=" + m_domain + " LoginContext=" + m_logincontext + " ServiceLoginContext="+ m_servicelogincontext+ + " GSS-API Principal="+ m_serviceprincipal+ " RequireLocalAccount=" + m_requirelocalaccount + " KnownUserMsg=" + m_knownusermsg ); + // show the whole config if set + // system locations will read NULL if not set (system defaults will be used) + if (kerberosshowconfig) + { + M_log.info(this + ".init()" + " SakaiHome=" + sakaihomepath + " SakaiPropertyKrb5Conf=" + kerberoskrb5conf + + " SakaiPropertyAuthLoginConfig=" + kerberosauthloginconfig + " SystemPropertyKrb5Conf=" + + System.getProperty("java.security.krb5.conf") + " SystemPropertyAuthLoginConfig=" + + System.getProperty("java.security.auth.login.config")); } - catch (Throwable t) + if (!m_requirelocalaccount && m_domain == null) { - M_log.warn(this + ".init(): ", t); + throw new IllegalStateException("If you don't require local accounts you must set the domain for email addresses."); } + } // init /** @@ -280,6 +284,7 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider */ public boolean findUserByEmail(UserEdit edit, String email) { + if (m_requirelocalaccount) return false; // lets not get messed up with spaces or cases String test = email.toLowerCase().trim(); @@ -308,66 +313,15 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider */ public boolean authenticateUser(String userId, UserEdit edit, String password) { - // The in-memory caching mechanism is implemented here - // try to get user from in-memory hashtable try { - UserData existingUser = (UserData) users.get(userId); - - boolean authUser = false; - String hpassword = encodeSHA(password); - - // Check for user in in-memory hashtable. To be a "valid, previously authenticated" user, - // 3 conditions must be met: - // - // 1) an entry for the userId must exist in the cache - // 2) the last usccessful authentication was < cachettl milliseconds ago - // 3) the one-way hash of the entered password must be equivalent to what is stored in the cache - // - // If these conditions are not, the authentication is performed via JAAS and, if sucessful, a new entry is created - - if (existingUser == null || (System.currentTimeMillis() - existingUser.getTimeStamp()) > m_cachettl - || !(existingUser.getHpw().equals(hpassword))) - { - if (M_log.isDebugEnabled()) M_log.debug("authenticateUser(): user " + userId + " not in table, querying Kerberos"); - - boolean authKerb = authenticateKerberos(userId, password); - - // if authentication succeeds, create entry for authenticated user in cache; - // otherwise, remove any entries for this user from cache - - if (authKerb) - { - if (M_log.isDebugEnabled()) - M_log.debug("authenticateUser(): putting authenticated user (" + userId + ") in table for caching"); - - UserData u = new UserData(); // create entry for authenticated user in cache - u.setId(userId); - u.setHpw(hpassword); - u.setTimeStamp(System.currentTimeMillis()); - users.put(userId, u); // put entry for authenticated user into cache - - } - else - { - users.remove(userId); - } - - authUser = authKerb; - - } - else - { - if (M_log.isDebugEnabled()) - M_log.debug("authenticateUser(): found authenticated user (" + existingUser.getId() + ") in table"); - authUser = true; - } - - return authUser; + JassAuthenticate jass = new JassAuthenticate(m_serviceprincipal, m_servicelogincontext, m_logincontext); + boolean authKerb = jass.attemptAuthentication(userId, password); + return authKerb; } catch (Exception e) { - if (M_log.isDebugEnabled()) M_log.debug("authenticateUser(): exception: " + e); + M_log.warn("authenticateUser(): exception: ", e); return false; } } // authenticateUser @@ -377,63 +331,6 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider *********************************************************************************************************************************************************************************************************************************************************/ /** - * Authenticate the user id and pw with Kerberos. - * - * @param user - * The user id. - * @param password - * the user supplied password. - * @return true if successful, false if not. - */ - protected boolean authenticateKerberos(String user, String pw) - { - // assure some length to the password - if ((pw == null) || (pw.length() == 0)) return false; - - // Obtain a LoginContext, needed for authentication. Tell it - // to use the LoginModule implementation specified by the - // appropriate entry in the JAAS login configuration - // file and to also use the specified CallbackHandler. - LoginContext lc = null; - try - { - SakaiCallbackHandler t = new SakaiCallbackHandler(); - t.setId(user); - t.setPw(pw); - lc = new LoginContext(m_logincontext, t); - } - catch (LoginException le) - { - if (M_log.isDebugEnabled()) M_log.debug("authenticateKerberos(): " + le.toString()); - return false; - } - catch (SecurityException se) - { - if (M_log.isDebugEnabled()) M_log.debug("authenticateKerberos(): " + se.toString()); - return false; - } - - try - { - // attempt authentication - lc.login(); - lc.logout(); - - if (M_log.isDebugEnabled()) M_log.debug("authenticateKerberos(" + user + ", pw): Kerberos auth success"); - - return true; - } - catch (LoginException le) - { - if (M_log.isDebugEnabled()) - M_log.debug("authenticateKerberos(" + user + ", pw): Kerberos auth failed: " + le.toString()); - - return false; - } - - } // authenticateKerberos - - /** * Check if the user id is known to kerberos. * * @param user @@ -602,101 +499,5 @@ public class KerberosUserDirectoryProvider implements UserDirectoryProvider return false; } - /** - *
- * Helper class for storing user data in an in-memory cache - *
- */ - class UserData - { - - String id; - - String hpw; - - long timeStamp; - - /** - * @return Returns the id. - */ - public String getId() - { - return id; - } - - /** - * @param id - * The id to set. - */ - public void setId(String id) - { - this.id = id; - } - - /** - * @param hpw - * hashed pw to put in. - */ - public void setHpw(String hpw) - { - this.hpw = hpw; - } - - /** - * @return Returns the hashed password. - */ - - public String getHpw() - { - return hpw; - } - - /** - * @return Returns the timeStamp. - */ - public long getTimeStamp() - { - return timeStamp; - } - - /** - * @param timeStamp - * The timeStamp to set. - */ - public void setTimeStamp(long timeStamp) - { - this.timeStamp = timeStamp; - } - - } // UserData class - - /** - *- * Hash string for storage in a cache using SHA - *
- * - * @param UTF-8 - * string - * @return encoded hash of string - */ - - private synchronized String encodeSHA(String plaintext) - { - - try - { - MessageDigest md = MessageDigest.getInstance("SHA"); - md.update(plaintext.getBytes("UTF-8")); - byte raw[] = md.digest(); - String hash = new String(Base64.encodeBase64(raw)); - return hash; - } - catch (Exception e) - { - M_log.warn("encodeSHA(): exception: " + e); - return null; - } - } // encodeSHA - } // KerberosUserDirectoryProvider diff --git a/kerberos/src/java/org/sakaiproject/component/kerberos/user/NullCallbackHandler.java b/kerberos/src/java/org/sakaiproject/component/kerberos/user/NullCallbackHandler.java new file mode 100644 index 0000000..6ee53e1 --- /dev/null +++ b/kerberos/src/java/org/sakaiproject/component/kerberos/user/NullCallbackHandler.java @@ -0,0 +1,25 @@ +package org.sakaiproject.component.kerberos.user; +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + + +/** + * Callback handler that doesn't support anything. This is used when the login is + * done using a keychain. + * @author buckett + * + */ +public class NullCallbackHandler implements CallbackHandler { + + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException { + for (Callback callback: callbacks) { + throw new UnsupportedCallbackException(callback, "Can't handle any callbacks"); + } + + } + +} diff --git a/kerberos/src/java/org/sakaiproject/component/kerberos/user/UsernamePasswordCallback.java b/kerberos/src/java/org/sakaiproject/component/kerberos/user/UsernamePasswordCallback.java new file mode 100644 index 0000000..eee19f8 --- /dev/null +++ b/kerberos/src/java/org/sakaiproject/component/kerberos/user/UsernamePasswordCallback.java @@ -0,0 +1,40 @@ +package org.sakaiproject.component.kerberos.user; +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** + * Simple callback handler that supplies a username and password. + * @author buckett + */ +public class UsernamePasswordCallback implements CallbackHandler { + + private final String username; + private final String password; + + public UsernamePasswordCallback(String username, String password) { + this.username = username; + this.password = password; + } + + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException { + for (Callback callback: callbacks) { + if (callback instanceof NameCallback) { + NameCallback nameCallback = (NameCallback)callback; + nameCallback.setName(username); + } else if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback)callback; + passwordCallback.setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, "Only username and password supported."); + } + } + + } + +} diff --git a/kerberos/src/test/org/sakaiproject/component/kerberos/user/SimpleJassAuthenticateTest.java b/kerberos/src/test/org/sakaiproject/component/kerberos/user/SimpleJassAuthenticateTest.java new file mode 100644 index 0000000..6d68a2f --- /dev/null +++ b/kerberos/src/test/org/sakaiproject/component/kerberos/user/SimpleJassAuthenticateTest.java @@ -0,0 +1,22 @@ +package org.sakaiproject.component.kerberos.user; + +import junit.framework.TestCase; + +public class SimpleJassAuthenticateTest extends TestCase { + + private JassAuthenticate jass; + + public void setUp() throws Exception { + super.setUp(); + jass = new JassAuthenticate("sakai@bit.oucs.ox.ac.uk", "servicePrincipal", "userPrincipal"); + } + + public void testGood() { + assertTrue(jass.attemptAuthentication("username", "password")); + } + + public void testBad() { + assertFalse(jass.attemptAuthentication("username", "wrong password")); + } + +} diff --git a/kerberos/src/test/org/sakaiproject/component/kerberos/user/ThreadedJaasAuthenticateTest.java b/kerberos/src/test/org/sakaiproject/component/kerberos/user/ThreadedJaasAuthenticateTest.java new file mode 100644 index 0000000..0bb2123 --- /dev/null +++ b/kerberos/src/test/org/sakaiproject/component/kerberos/user/ThreadedJaasAuthenticateTest.java @@ -0,0 +1,114 @@ +package org.sakaiproject.component.kerberos.user; + +import java.io.IOException; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Properties; +import java.util.Random; + +import org.omg.CORBA.portable.ApplicationException; +import org.omg.CORBA_2_3.portable.OutputStream; + +import com.sun.corba.se.impl.presentation.rmi.ExceptionHandler; + +import junit.framework.TestCase; + +public class ThreadedJaasAuthenticateTest extends TestCase { + + private int loopLimit = 1000; + private int threadCount = 10; + + private String goodUser; + private String goodPass; + + private String badUser; + private String badPass; + private String multipleUser; + private String multiplePass; + + public ThreadedJaasAuthenticateTest() { + Properties props = new Properties(); + try { + props.load(getClass().getResourceAsStream("/users.properties")); + } catch (IOException e) { + throw new IllegalStateException("Can't load users file.", e); + } + goodUser = props.getProperty("good.user"); + goodPass = props.getProperty("good.pass"); + badUser = props.getProperty("bad.user"); + badPass = props.getProperty("bad.pass"); + + multipleUser = props.getProperty("multiple.user"); + multiplePass = props.getProperty("multiple.pass"); + } + + + protected void setUp() throws Exception { + super.setUp(); + } + + public void testUserThread() throws InterruptedException { + Thread[] goodThreads = new Thread[threadCount]; + for (int i = 0; i < threadCount ; i++) { + String name = "Thread-"+ i+ "-good"; + goodThreads[i] = new Thread(new Authenticate(multipleUser+i, multiplePass, true), name); + goodThreads[i].start(); + System.out.println("Started "+ name); + } + Thread[] badThreads = new Thread[threadCount]; + for (int i = 0; i < threadCount ; i++) { + String name = "Thread-"+ i+ "-bad"; + badThreads[i] = new Thread(new Authenticate(badUser, badPass, false), name); + badThreads[i].start(); + System.out.println("Started "+ name); + } + + for (Thread thread: goodThreads) { + thread.join(); + } + for (Thread thread: badThreads) { + thread.join(); + } + } + + public void testThreads() throws InterruptedException { + Thread[] threads = new Thread[threadCount]; + Random rnd = new Random(); + for (int i = 0; i < threadCount ; i++) { + String name; + if (rnd.nextBoolean()) { + name = "Thread-"+ i+ "-good"; + threads[i] = new Thread(new Authenticate(goodUser, goodPass, true), name); + } else { + name = "Thread-"+ i+ "-bad"; + threads[i] = new Thread(new Authenticate(badUser, badPass, false), name); + } + threads[i].start(); + System.out.println("Started "+ name); + } + for (Thread thread: threads) { + thread.join(); + } + } + + private class Authenticate implements Runnable { + + String username; + String password; + boolean good; + + private Authenticate(String username, String password, boolean good) { + this.username = username; + this.password = password; + this.good = good; + } + + public void run() { + for(int i = 0; i< loopLimit; i++) { + JassAuthenticate jass = new JassAuthenticate("sakai@bit.oucs.ox.ac.uk", "servicePrincipal", "userPrincipal"); + assertEquals(good,jass.attemptAuthentication(username, password)); + } + } + + } + +} diff --git a/kerberos/src/test/sakai-jaas.conf b/kerberos/src/test/sakai-jaas.conf new file mode 100644 index 0000000..8a36c17 --- /dev/null +++ b/kerberos/src/test/sakai-jaas.conf @@ -0,0 +1,22 @@ +/* + * JAAS Login Configuration for Sakai + */ + +userPrincipal { + com.sun.security.auth.module.Krb5LoginModule required + storeKey="true" + useTicketCache="false"; +}; + +servicePrincipal { + com.sun.security.auth.module.Krb5LoginModule required + doNotPrompt="true" + principal="sakai/bit.oucs.ox.ac.uk" + useKeyTab="true" + keyTab="/Users/buckett/krb5-test.keytab" + storeKey="true" // Store the key inside the subject + // refreshKrb5Config="true" + isInitiator="false" // JDK 6 Only + useTicketCache="false"; +}; +