001 /* 002 * Copyright 2010 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package com.hs.mail.security.login; 017 018 import java.security.Principal; 019 import java.text.MessageFormat; 020 import java.util.Hashtable; 021 import java.util.Map; 022 023 import javax.naming.AuthenticationException; 024 import javax.naming.Context; 025 import javax.naming.NamingEnumeration; 026 import javax.naming.NamingException; 027 import javax.naming.directory.DirContext; 028 import javax.naming.directory.InitialDirContext; 029 import javax.naming.directory.SearchControls; 030 import javax.naming.directory.SearchResult; 031 import javax.security.auth.Subject; 032 import javax.security.auth.callback.Callback; 033 import javax.security.auth.callback.CallbackHandler; 034 import javax.security.auth.callback.NameCallback; 035 import javax.security.auth.callback.PasswordCallback; 036 import javax.security.auth.login.CredentialException; 037 import javax.security.auth.login.LoginException; 038 039 import org.apache.commons.lang.StringUtils; 040 041 import com.hs.mail.security.UserPrincipal; 042 043 /** 044 * A LoginModule that allows for authentication based on LDAP directory. 045 * 046 * @author Won Chul Doh 047 * @since Aug 7, 2010 048 * 049 */ 050 public class JndiLoginModule extends BasicLoginModule { 051 052 private String contextFactory; 053 private String url; 054 private String username; 055 private String password; 056 private String authentication; 057 private String base; 058 private String searchFilter; 059 private String returnAttribute; 060 private boolean subtree; 061 private MessageFormat searchFilterFormat; 062 protected DirContext context = null; 063 064 @Override 065 public void initialize(Subject subject, CallbackHandler callbackHandler, 066 Map<String, ?> sharedState, Map<String, ?> options) { 067 super.initialize(subject, callbackHandler, sharedState, options); 068 contextFactory = getOption("context.factory", "com.sun.jndi.ldap.LdapCtxFactory"); 069 url = getOption("url", null); 070 if (url == null) { 071 throw new Error("No JNDI URL specified"); 072 } 073 username = getOption("username", null); 074 password = getOption("password", null); 075 authentication = getOption("authentication", "simple"); 076 base = getOption("base", null); 077 String filter = getOption("searchFilter", "(uid={0})"); 078 searchFilterFormat = new MessageFormat(filter); 079 returnAttribute = getOption("returnAttribute", null); 080 subtree = new Boolean(getOption("subtree", "true")).booleanValue(); 081 } 082 083 @Override 084 protected Principal[] validate(Callback[] callbacks) throws LoginException { 085 String username = ((NameCallback) callbacks[0]).getName(); 086 char[] password = ((PasswordCallback) callbacks[1]).getPassword(); 087 088 Principal[] principals = new Principal[1]; 089 principals[0] = new UserPrincipal(username); 090 try { 091 boolean ok = authenticate(username, String.valueOf(password)); 092 if (!ok) 093 throw new CredentialException("Incorrect password for " 094 + username); 095 else 096 return principals; 097 } catch (Exception e) { 098 throw (LoginException) new LoginException("LDAP Error") 099 .initCause(e); 100 } 101 } 102 103 @SuppressWarnings("unchecked") 104 protected boolean authenticate(String username, String password) 105 throws Exception { 106 DirContext context = null; 107 try { 108 context = open(); 109 searchFilterFormat.format(new String[] { username }); 110 SearchControls constraints = new SearchControls(); 111 constraints.setSearchScope(subtree ? SearchControls.SUBTREE_SCOPE 112 : SearchControls.ONELEVEL_SCOPE); 113 if (returnAttribute != null) { 114 String[] attribs = StringUtils.split(returnAttribute, ","); 115 constraints.setReturningAttributes(attribs); 116 } 117 NamingEnumeration ne = context.search(base, searchFilter, 118 constraints); 119 if (ne == null || !ne.hasMore()) { 120 return false; 121 } 122 SearchResult sr = (SearchResult) ne.next(); 123 if (ne.hasMore()) { 124 // Ignore for now 125 } 126 // Check the credentials by binding to server 127 if (bindUser(context, sr.getNameInNamespace(), password)) { 128 return true; 129 } else { 130 return true; 131 } 132 } catch (NamingException e) { 133 close(context); 134 return false; 135 } 136 } 137 138 @SuppressWarnings("unchecked") 139 protected DirContext open() throws NamingException { 140 if (context == null) { 141 try { 142 // Set up the environment for creating the initial context 143 Hashtable env = new Hashtable(); 144 env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); 145 if (StringUtils.isNotEmpty(username)) { 146 env.put(Context.SECURITY_PRINCIPAL, username); 147 } 148 if (StringUtils.isNotEmpty(password)) { 149 env.put(Context.SECURITY_CREDENTIALS, password); 150 } 151 env.put(Context.PROVIDER_URL, url); 152 env.put(Context.SECURITY_AUTHENTICATION, authentication); 153 context = new InitialDirContext(env); 154 } catch (NamingException e) { 155 throw e; 156 } 157 } 158 return context; 159 } 160 161 private boolean bindUser(DirContext context, String dn, String password) 162 throws NamingException { 163 boolean isValid = false; 164 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 165 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 166 try { 167 context.getAttributes("", null); 168 isValid = true; 169 } catch (AuthenticationException e) { 170 } 171 if (StringUtils.isNotEmpty(this.username)) { 172 context.addToEnvironment(Context.SECURITY_PRINCIPAL, this.username); 173 } else { 174 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 175 } 176 if (StringUtils.isNotEmpty(this.password)) { 177 context.addToEnvironment(Context.SECURITY_CREDENTIALS, 178 this.password); 179 } else { 180 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 181 } 182 return isValid; 183 } 184 185 protected void close(DirContext context) { 186 if (context != null) { 187 try { 188 context.close(); 189 context = null; 190 } catch (Exception e) { 191 } 192 } 193 } 194 195 }