001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package org.apache.geronimo.security.realm.providers;
019
020 import java.io.IOException;
021 import java.security.MessageDigest;
022 import java.security.NoSuchAlgorithmException;
023 import java.security.Principal;
024 import java.sql.Connection;
025 import java.sql.Driver;
026 import java.sql.PreparedStatement;
027 import java.sql.ResultSet;
028 import java.sql.SQLException;
029 import java.util.Arrays;
030 import java.util.Collections;
031 import java.util.HashSet;
032 import java.util.List;
033 import java.util.Map;
034 import java.util.Properties;
035 import java.util.Set;
036
037 import javax.security.auth.Subject;
038 import javax.security.auth.callback.Callback;
039 import javax.security.auth.callback.CallbackHandler;
040 import javax.security.auth.callback.NameCallback;
041 import javax.security.auth.callback.PasswordCallback;
042 import javax.security.auth.callback.UnsupportedCallbackException;
043 import javax.security.auth.login.FailedLoginException;
044 import javax.security.auth.login.LoginException;
045 import javax.security.auth.spi.LoginModule;
046 import javax.sql.DataSource;
047
048 import org.apache.commons.logging.Log;
049 import org.apache.commons.logging.LogFactory;
050 import org.apache.geronimo.gbean.AbstractName;
051 import org.apache.geronimo.gbean.AbstractNameQuery;
052 import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
053 import org.apache.geronimo.kernel.GBeanNotFoundException;
054 import org.apache.geronimo.kernel.Kernel;
055 import org.apache.geronimo.kernel.KernelRegistry;
056 import org.apache.geronimo.management.geronimo.JCAManagedConnectionFactory;
057 import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
058 import org.apache.geronimo.security.jaas.WrappingLoginModule;
059 import org.apache.geronimo.crypto.encoders.Base64;
060 import org.apache.geronimo.crypto.encoders.HexTranslator;
061
062
063 /**
064 * A login module that loads security information from a SQL database. Expects
065 * to be run by a GenericSecurityRealm (doesn't work on its own).
066 * <p/>
067 * This requires database connectivity information (either 1: a dataSourceName and
068 * optional dataSourceApplication or 2: a JDBC driver, URL, username, and password)
069 * and 2 SQL queries.
070 * <p/>
071 * The userSelect query should return 2 values, the username and the password in
072 * that order. It should include one PreparedStatement parameter (a ?) which
073 * will be filled in with the username. In other words, the query should look
074 * like: <tt>SELECT user, password FROM credentials WHERE username=?</tt>
075 * <p/>
076 * The groupSelect query should return 2 values, the username and the group name in
077 * that order (but it may return multiple rows, one per group). It should include
078 * one PreparedStatement parameter (a ?) which will be filled in with the username.
079 * In other words, the query should look like:
080 * <tt>SELECT user, role FROM user_roles WHERE username=?</tt>
081 * <p/>
082 * This login module checks security credentials so the lifecycle methods must return true to indicate success
083 * or throw LoginException to indicate failure.
084 *
085 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
086 */
087 public class SQLLoginModule implements LoginModule {
088 private static Log log = LogFactory.getLog(SQLLoginModule.class);
089 public final static String USER_SELECT = "userSelect";
090 public final static String GROUP_SELECT = "groupSelect";
091 public final static String CONNECTION_URL = "jdbcURL";
092 public final static String USER = "jdbcUser";
093 public final static String PASSWORD = "jdbcPassword";
094 public final static String DRIVER = "jdbcDriver";
095 public final static String DATABASE_POOL_NAME = "dataSourceName";
096 public final static String DATABASE_POOL_APP_NAME = "dataSourceApplication";
097 public final static String DIGEST = "digest";
098 public final static String ENCODING = "encoding";
099 public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(USER_SELECT, GROUP_SELECT, CONNECTION_URL,
100 USER, PASSWORD, DRIVER, DATABASE_POOL_NAME, DATABASE_POOL_APP_NAME, DIGEST, ENCODING));
101
102 private String connectionURL;
103 private Properties properties;
104 private Driver driver;
105 private JCAManagedConnectionFactory factory;
106 private String userSelect;
107 private String groupSelect;
108 private String digest;
109 private String encoding;
110
111 private boolean loginSucceeded;
112 private Subject subject;
113 private CallbackHandler handler;
114 private String cbUsername;
115 private String cbPassword;
116 private final Set<String> groups = new HashSet<String>();
117 private final Set<Principal> allPrincipals = new HashSet<Principal>();
118
119 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
120 this.subject = subject;
121 this.handler = callbackHandler;
122 for(Object option: options.keySet()) {
123 if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option)
124 && !WrappingLoginModule.supportedOptions.contains(option)) {
125 log.warn("Ignoring option: "+option+". Not supported.");
126 }
127 }
128 userSelect = (String) options.get(USER_SELECT);
129 groupSelect = (String) options.get(GROUP_SELECT);
130
131 digest = (String) options.get(DIGEST);
132 encoding = (String) options.get(ENCODING);
133 if (digest != null && !digest.equals("")) {
134 // Check if the digest algorithm is available
135 try {
136 MessageDigest.getInstance(digest);
137 } catch (NoSuchAlgorithmException e) {
138 log.error("Initialization failed. Digest algorithm " + digest + " is not available.", e);
139 throw new IllegalArgumentException("Unable to configure SQL login module: " + e.getMessage(), e);
140 }
141 if (encoding != null && !"hex".equalsIgnoreCase(encoding) && !"base64".equalsIgnoreCase(encoding)) {
142 log.error("Initialization failed. Digest Encoding " + encoding + " is not supported.");
143 throw new IllegalArgumentException(
144 "Unable to configure SQL login module. Digest Encoding " + encoding + " not supported.");
145 }
146 }
147
148 String dataSourceName = (String) options.get(DATABASE_POOL_NAME);
149 if (dataSourceName != null) {
150 dataSourceName = dataSourceName.trim();
151 String dataSourceAppName = (String) options.get(DATABASE_POOL_APP_NAME);
152 if (dataSourceAppName == null || dataSourceAppName.trim().equals("")) {
153 dataSourceAppName = "null";
154 } else {
155 dataSourceAppName = dataSourceAppName.trim();
156 }
157 String kernelName = (String) options.get(JaasLoginModuleUse.KERNEL_NAME_LM_OPTION);
158 Kernel kernel = KernelRegistry.getKernel(kernelName);
159 Set<AbstractName> set = kernel.listGBeans(new AbstractNameQuery(JCAManagedConnectionFactory.class.getName()));
160 JCAManagedConnectionFactory factory;
161 for (AbstractName name : set) {
162 if (name.getName().get(NameFactory.J2EE_APPLICATION).equals(dataSourceAppName) &&
163 name.getName().get(NameFactory.J2EE_NAME).equals(dataSourceName)) {
164 try {
165 factory = (JCAManagedConnectionFactory) kernel.getGBean(name);
166 String type = factory.getConnectionFactoryInterface();
167 if (type.equals(DataSource.class.getName())) {
168 this.factory = factory;
169 break;
170 }
171 } catch (GBeanNotFoundException e) {
172 // ignore... GBean was unregistered
173 }
174 }
175 }
176 } else {
177 connectionURL = (String) options.get(CONNECTION_URL);
178 properties = new Properties();
179 if (options.get(USER) != null) {
180 properties.put("user", options.get(USER));
181 }
182 if (options.get(PASSWORD) != null) {
183 properties.put("password", options.get(PASSWORD));
184 }
185 ClassLoader cl = (ClassLoader) options.get(JaasLoginModuleUse.CLASSLOADER_LM_OPTION);
186 try {
187 driver = (Driver) cl.loadClass((String) options.get(DRIVER)).newInstance();
188 } catch (ClassNotFoundException e) {
189 throw new IllegalArgumentException("Driver class " + options.get(
190 DRIVER) + " is not available. Perhaps you need to add it as a dependency in your deployment plan?",
191 e);
192 } catch (Exception e) {
193 throw new IllegalArgumentException(
194 "Unable to load, instantiate, register driver " + options.get(DRIVER) + ": " + e.getMessage(),
195 e);
196 }
197 }
198 }
199
200 /**
201 * This LoginModule is not to be ignored. So, this method should never return false.
202 * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
203 * if authentication fails
204 */
205 public boolean login() throws LoginException {
206 loginSucceeded = false;
207 Callback[] callbacks = new Callback[2];
208
209 callbacks[0] = new NameCallback("User name");
210 callbacks[1] = new PasswordCallback("Password", false);
211 try {
212 handler.handle(callbacks);
213 } catch (IOException ioe) {
214 throw (LoginException) new LoginException().initCause(ioe);
215 } catch (UnsupportedCallbackException uce) {
216 throw (LoginException) new LoginException().initCause(uce);
217 }
218 assert callbacks.length == 2;
219 cbUsername = ((NameCallback) callbacks[0]).getName();
220 if (cbUsername == null || cbUsername.equals("")) {
221 throw new FailedLoginException();
222 }
223 char[] provided = ((PasswordCallback) callbacks[1]).getPassword();
224 cbPassword = provided == null ? null : new String(provided);
225
226 try {
227 Connection conn;
228 if (factory != null) {
229 DataSource ds = (DataSource) factory.getConnectionFactory();
230 conn = ds.getConnection();
231 } else {
232 conn = driver.connect(connectionURL, properties);
233 }
234
235 try {
236 PreparedStatement statement = conn.prepareStatement(userSelect);
237 try {
238 int count = countParameters(userSelect);
239 for (int i = 0; i < count; i++) {
240 statement.setObject(i + 1, cbUsername);
241 }
242 ResultSet result = statement.executeQuery();
243
244 try {
245 boolean found = false;
246 while (result.next()) {
247 String userName = result.getString(1);
248 String userPassword = result.getString(2);
249
250 if (cbUsername.equals(userName)) {
251 found = true;
252 if (!checkPassword(userPassword, cbPassword)) {
253 throw new FailedLoginException();
254 }
255 break;
256 }
257 }
258 if(!found) {
259 // User does not exist
260 throw new FailedLoginException();
261 }
262 } finally {
263 result.close();
264 }
265 } finally {
266 statement.close();
267 }
268
269 statement = conn.prepareStatement(groupSelect);
270 try {
271 int count = countParameters(groupSelect);
272 for (int i = 0; i < count; i++) {
273 statement.setObject(i + 1, cbUsername);
274 }
275 ResultSet result = statement.executeQuery();
276
277 try {
278 while (result.next()) {
279 String userName = result.getString(1);
280 String groupName = result.getString(2);
281
282 if (cbUsername.equals(userName)) {
283 groups.add(groupName);
284 }
285 }
286 } finally {
287 result.close();
288 }
289 } finally {
290 statement.close();
291 }
292 } finally {
293 conn.close();
294 }
295 } catch (LoginException e) {
296 // Clear out the private state
297 cbUsername = null;
298 cbPassword = null;
299 groups.clear();
300 throw e;
301 } catch (SQLException sqle) {
302 // Clear out the private state
303 cbUsername = null;
304 cbPassword = null;
305 groups.clear();
306 throw (LoginException) new LoginException("SQL error").initCause(sqle);
307 } catch (Exception e) {
308 // Clear out the private state
309 cbUsername = null;
310 cbPassword = null;
311 groups.clear();
312 throw (LoginException) new LoginException("Could not access datasource").initCause(e);
313 }
314
315 loginSucceeded = true;
316 return true;
317 }
318
319 /*
320 * @exception LoginException if login succeeded but commit failed.
321 *
322 * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
323 */
324 public boolean commit() throws LoginException {
325 if(loginSucceeded) {
326 if(cbUsername != null) {
327 allPrincipals.add(new GeronimoUserPrincipal(cbUsername));
328 }
329 for(String group: groups) {
330 allPrincipals.add(new GeronimoGroupPrincipal(group));
331 }
332 subject.getPrincipals().addAll(allPrincipals);
333 }
334
335 // Clear out the private state
336 cbUsername = null;
337 cbPassword = null;
338 groups.clear();
339
340 return loginSucceeded;
341 }
342
343 public boolean abort() throws LoginException {
344 if(loginSucceeded) {
345 // Clear out the private state
346 cbUsername = null;
347 cbPassword = null;
348 groups.clear();
349 allPrincipals.clear();
350 }
351 return loginSucceeded;
352 }
353
354 public boolean logout() throws LoginException {
355 // Clear out the private state
356 loginSucceeded = false;
357 cbUsername = null;
358 cbPassword = null;
359 groups.clear();
360 if(!subject.isReadOnly()) {
361 // Remove principals added by this LoginModule
362 subject.getPrincipals().removeAll(allPrincipals);
363 }
364 allPrincipals.clear();
365 return true;
366 }
367
368 private static int countParameters(String sql) {
369 int count = 0;
370 int pos = -1;
371 while ((pos = sql.indexOf('?', pos + 1)) != -1) {
372 ++count;
373 }
374 return count;
375 }
376
377 /**
378 * This method checks if the provided password is correct. The original password may have been digested.
379 *
380 * @param real Original password in digested form if applicable
381 * @param provided User provided password in clear text
382 * @return true If the password is correct
383 */
384 private boolean checkPassword(String real, String provided) {
385 if (real == null && provided == null) {
386 return true;
387 }
388 if (real == null || provided == null) {
389 return false;
390 }
391
392 //both are non-null
393 if (digest == null || digest.equals("")) {
394 // No digest algorithm is used
395 return real.equals(provided);
396 }
397 try {
398 // Digest the user provided password
399 MessageDigest md = MessageDigest.getInstance(digest);
400 byte[] data = md.digest(provided.getBytes());
401 if (encoding == null || "hex".equalsIgnoreCase(encoding)) {
402 // Convert bytes to hex digits
403 byte[] hexData = new byte[data.length * 2];
404 HexTranslator ht = new HexTranslator();
405 ht.encode(data, 0, data.length, hexData, 0);
406 // Compare the digested provided password with the actual one
407 return real.equalsIgnoreCase(new String(hexData));
408 } else if ("base64".equalsIgnoreCase(encoding)) {
409 return real.equals(new String(Base64.encode(data)));
410 }
411 } catch (NoSuchAlgorithmException e) {
412 // Should not occur. Availability of algorithm has been checked at initialization
413 log.error("Should not occur. Availability of algorithm has been checked at initialization.", e);
414 }
415 return false;
416 }
417 }