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    }