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 }