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.HashSet; 030 import java.util.Map; 031 import java.util.Properties; 032 import java.util.Set; 033 034 import javax.security.auth.Subject; 035 import javax.security.auth.callback.Callback; 036 import javax.security.auth.callback.CallbackHandler; 037 import javax.security.auth.callback.NameCallback; 038 import javax.security.auth.callback.PasswordCallback; 039 import javax.security.auth.callback.UnsupportedCallbackException; 040 import javax.security.auth.login.FailedLoginException; 041 import javax.security.auth.login.LoginException; 042 import javax.security.auth.spi.LoginModule; 043 import javax.sql.DataSource; 044 045 import org.apache.commons.logging.Log; 046 import org.apache.commons.logging.LogFactory; 047 import org.apache.geronimo.gbean.AbstractName; 048 import org.apache.geronimo.gbean.AbstractNameQuery; 049 import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory; 050 import org.apache.geronimo.kernel.GBeanNotFoundException; 051 import org.apache.geronimo.kernel.Kernel; 052 import org.apache.geronimo.kernel.KernelRegistry; 053 import org.apache.geronimo.management.geronimo.JCAManagedConnectionFactory; 054 import org.apache.geronimo.security.jaas.JaasLoginModuleUse; 055 import org.apache.geronimo.util.encoders.Base64; 056 import org.apache.geronimo.util.encoders.HexTranslator; 057 058 059 /** 060 * A login module that loads security information from a SQL database. Expects 061 * to be run by a GenericSecurityRealm (doesn't work on its own). 062 * <p/> 063 * This requires database connectivity information (either 1: a dataSourceName and 064 * optional dataSourceApplication or 2: a JDBC driver, URL, username, and password) 065 * and 2 SQL queries. 066 * <p/> 067 * The userSelect query should return 2 values, the username and the password in 068 * that order. It should include one PreparedStatement parameter (a ?) which 069 * will be filled in with the username. In other words, the query should look 070 * like: <tt>SELECT user, password FROM credentials WHERE username=?</tt> 071 * <p/> 072 * The groupSelect query should return 2 values, the username and the group name in 073 * that order (but it may return multiple rows, one per group). It should include 074 * one PreparedStatement parameter (a ?) which will be filled in with the username. 075 * In other words, the query should look like: 076 * <tt>SELECT user, role FROM user_roles WHERE username=?</tt> 077 * <p/> 078 * This login module checks security credentials so the lifecycle methods must return true to indicate success 079 * or throw LoginException to indicate failure. 080 * 081 * @version $Rev: 565912 $ $Date: 2007-08-14 17:03:11 -0400 (Tue, 14 Aug 2007) $ 082 */ 083 public class SQLLoginModule implements LoginModule { 084 private static Log log = LogFactory.getLog(SQLLoginModule.class); 085 public final static String USER_SELECT = "userSelect"; 086 public final static String GROUP_SELECT = "groupSelect"; 087 public final static String CONNECTION_URL = "jdbcURL"; 088 public final static String USER = "jdbcUser"; 089 public final static String PASSWORD = "jdbcPassword"; 090 public final static String DRIVER = "jdbcDriver"; 091 public final static String DATABASE_POOL_NAME = "dataSourceName"; 092 public final static String DATABASE_POOL_APP_NAME = "dataSourceApplication"; 093 public final static String DIGEST = "digest"; 094 public final static String ENCODING = "encoding"; 095 private String connectionURL; 096 private Properties properties; 097 private Driver driver; 098 private JCAManagedConnectionFactory factory; 099 private String userSelect; 100 private String groupSelect; 101 private String digest; 102 private String encoding; 103 104 private Subject subject; 105 private CallbackHandler handler; 106 private String cbUsername; 107 private String cbPassword; 108 private final Set<Principal> groups = new HashSet<Principal>(); 109 110 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 111 this.subject = subject; 112 this.handler = callbackHandler; 113 userSelect = (String) options.get(USER_SELECT); 114 groupSelect = (String) options.get(GROUP_SELECT); 115 116 digest = (String) options.get(DIGEST); 117 encoding = (String) options.get(ENCODING); 118 if (digest != null && !digest.equals("")) { 119 // Check if the digest algorithm is available 120 try { 121 MessageDigest.getInstance(digest); 122 } catch (NoSuchAlgorithmException e) { 123 log.error("Initialization failed. Digest algorithm " + digest + " is not available.", e); 124 throw new IllegalArgumentException("Unable to configure SQL login module: " + e.getMessage(), e); 125 } 126 if (encoding != null && !"hex".equalsIgnoreCase(encoding) && !"base64".equalsIgnoreCase(encoding)) { 127 log.error("Initialization failed. Digest Encoding " + encoding + " is not supported."); 128 throw new IllegalArgumentException( 129 "Unable to configure SQL login module. Digest Encoding " + encoding + " not supported."); 130 } 131 } 132 133 String dataSourceName = (String) options.get(DATABASE_POOL_NAME); 134 if (dataSourceName != null) { 135 dataSourceName = dataSourceName.trim(); 136 String dataSourceAppName = (String) options.get(DATABASE_POOL_APP_NAME); 137 if (dataSourceAppName == null || dataSourceAppName.trim().equals("")) { 138 dataSourceAppName = "null"; 139 } else { 140 dataSourceAppName = dataSourceAppName.trim(); 141 } 142 String kernelName = (String) options.get(JaasLoginModuleUse.KERNEL_NAME_LM_OPTION); 143 Kernel kernel = KernelRegistry.getKernel(kernelName); 144 Set<AbstractName> set = kernel.listGBeans(new AbstractNameQuery(JCAManagedConnectionFactory.class.getName())); 145 JCAManagedConnectionFactory factory; 146 for (AbstractName name : set) { 147 if (name.getName().get(NameFactory.J2EE_APPLICATION).equals(dataSourceAppName) && 148 name.getName().get(NameFactory.J2EE_NAME).equals(dataSourceName)) { 149 try { 150 factory = (JCAManagedConnectionFactory) kernel.getGBean(name); 151 String type = factory.getConnectionFactoryInterface(); 152 if (type.equals(DataSource.class.getName())) { 153 this.factory = factory; 154 break; 155 } 156 } catch (GBeanNotFoundException e) { 157 // ignore... GBean was unregistered 158 } 159 } 160 } 161 } else { 162 connectionURL = (String) options.get(CONNECTION_URL); 163 properties = new Properties(); 164 if (options.get(USER) != null) { 165 properties.put("user", options.get(USER)); 166 } 167 if (options.get(PASSWORD) != null) { 168 properties.put("password", options.get(PASSWORD)); 169 } 170 ClassLoader cl = (ClassLoader) options.get(JaasLoginModuleUse.CLASSLOADER_LM_OPTION); 171 try { 172 driver = (Driver) cl.loadClass((String) options.get(DRIVER)).newInstance(); 173 } catch (ClassNotFoundException e) { 174 throw new IllegalArgumentException("Driver class " + options.get( 175 DRIVER) + " is not available. Perhaps you need to add it as a dependency in your deployment plan?", 176 e); 177 } catch (Exception e) { 178 throw new IllegalArgumentException( 179 "Unable to load, instantiate, register driver " + options.get(DRIVER) + ": " + e.getMessage(), 180 e); 181 } 182 } 183 } 184 185 public boolean login() throws LoginException { 186 Callback[] callbacks = new Callback[2]; 187 188 callbacks[0] = new NameCallback("User name"); 189 callbacks[1] = new PasswordCallback("Password", false); 190 try { 191 handler.handle(callbacks); 192 } catch (IOException ioe) { 193 throw (LoginException) new LoginException().initCause(ioe); 194 } catch (UnsupportedCallbackException uce) { 195 throw (LoginException) new LoginException().initCause(uce); 196 } 197 assert callbacks.length == 2; 198 cbUsername = ((NameCallback) callbacks[0]).getName(); 199 if (cbUsername == null || cbUsername.equals("")) { 200 throw new FailedLoginException(); 201 } 202 char[] provided = ((PasswordCallback) callbacks[1]).getPassword(); 203 cbPassword = provided == null ? null : new String(provided); 204 205 try { 206 Connection conn; 207 if (factory != null) { 208 DataSource ds = (DataSource) factory.getConnectionFactory(); 209 conn = ds.getConnection(); 210 } else { 211 conn = driver.connect(connectionURL, properties); 212 } 213 214 try { 215 PreparedStatement statement = conn.prepareStatement(userSelect); 216 try { 217 int count = countParameters(userSelect); 218 for (int i = 0; i < count; i++) { 219 statement.setObject(i + 1, cbUsername); 220 } 221 ResultSet result = statement.executeQuery(); 222 223 try { 224 while (result.next()) { 225 String userName = result.getString(1); 226 String userPassword = result.getString(2); 227 228 if (cbUsername.equals(userName)) { 229 if (!checkPassword(userPassword, cbPassword)) { 230 throw new FailedLoginException(); 231 } 232 break; 233 } 234 } 235 } finally { 236 result.close(); 237 } 238 } finally { 239 statement.close(); 240 } 241 242 statement = conn.prepareStatement(groupSelect); 243 try { 244 int count = countParameters(groupSelect); 245 for (int i = 0; i < count; i++) { 246 statement.setObject(i + 1, cbUsername); 247 } 248 ResultSet result = statement.executeQuery(); 249 250 try { 251 while (result.next()) { 252 String userName = result.getString(1); 253 String groupName = result.getString(2); 254 255 if (cbUsername.equals(userName)) { 256 groups.add(new GeronimoGroupPrincipal(groupName)); 257 } 258 } 259 } finally { 260 result.close(); 261 } 262 } finally { 263 statement.close(); 264 } 265 } finally { 266 conn.close(); 267 } 268 } catch (SQLException sqle) { 269 throw (LoginException) new LoginException("SQL error").initCause(sqle); 270 } catch (Exception e) { 271 throw (LoginException) new LoginException("Could not access datasource").initCause(e); 272 } 273 274 return true; 275 } 276 277 public boolean commit() throws LoginException { 278 Set<Principal> principals = subject.getPrincipals(); 279 principals.add(new GeronimoUserPrincipal(cbUsername)); 280 principals.addAll(groups); 281 282 return true; 283 } 284 285 public boolean abort() throws LoginException { 286 cbUsername = null; 287 cbPassword = null; 288 289 return true; 290 } 291 292 public boolean logout() throws LoginException { 293 cbUsername = null; 294 cbPassword = null; 295 //todo: should remove principals put in by commit 296 return true; 297 } 298 299 private static int countParameters(String sql) { 300 int count = 0; 301 int pos = -1; 302 while ((pos = sql.indexOf('?', pos + 1)) != -1) { 303 ++count; 304 } 305 return count; 306 } 307 308 /** 309 * This method checks if the provided password is correct. The original password may have been digested. 310 * 311 * @param real Original password in digested form if applicable 312 * @param provided User provided password in clear text 313 * @return true If the password is correct 314 */ 315 private boolean checkPassword(String real, String provided) { 316 if (real == null && provided == null) { 317 return true; 318 } 319 if (real == null || provided == null) { 320 return false; 321 } 322 323 //both are non-null 324 if (digest == null || digest.equals("")) { 325 // No digest algorithm is used 326 return real.equals(provided); 327 } 328 try { 329 // Digest the user provided password 330 MessageDigest md = MessageDigest.getInstance(digest); 331 byte[] data = md.digest(provided.getBytes()); 332 if (encoding == null || "hex".equalsIgnoreCase(encoding)) { 333 // Convert bytes to hex digits 334 byte[] hexData = new byte[data.length * 2]; 335 HexTranslator ht = new HexTranslator(); 336 ht.encode(data, 0, data.length, hexData, 0); 337 // Compare the digested provided password with the actual one 338 return real.equalsIgnoreCase(new String(hexData)); 339 } else if ("base64".equalsIgnoreCase(encoding)) { 340 return real.equals(new String(Base64.encode(data))); 341 } 342 } catch (NoSuchAlgorithmException e) { 343 // Should not occur. Availability of algorithm has been checked at initialization 344 log.error("Should not occur. Availability of algorithm has been checked at initialization.", e); 345 } 346 return false; 347 } 348 }