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.io.InputStream;
022    import java.net.URI;
023    import java.security.MessageDigest;
024    import java.security.NoSuchAlgorithmException;
025    import java.security.Principal;
026    import java.util.Enumeration;
027    import java.util.HashMap;
028    import java.util.HashSet;
029    import java.util.Map;
030    import java.util.Properties;
031    import java.util.Set;
032    
033    import javax.security.auth.Subject;
034    import javax.security.auth.callback.Callback;
035    import javax.security.auth.callback.CallbackHandler;
036    import javax.security.auth.callback.NameCallback;
037    import javax.security.auth.callback.PasswordCallback;
038    import javax.security.auth.callback.UnsupportedCallbackException;
039    import javax.security.auth.login.FailedLoginException;
040    import javax.security.auth.login.LoginException;
041    import javax.security.auth.spi.LoginModule;
042    
043    import org.apache.commons.logging.Log;
044    import org.apache.commons.logging.LogFactory;
045    import org.apache.geronimo.common.GeronimoSecurityException;
046    import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
047    import org.apache.geronimo.system.serverinfo.ServerInfo;
048    import org.apache.geronimo.util.SimpleEncryption;
049    import org.apache.geronimo.util.encoders.Base64;
050    import org.apache.geronimo.util.encoders.HexTranslator;
051    
052    
053    /**
054     * A LoginModule that reads a list of credentials and group from files on disk.  The
055     * files should be formatted using standard Java properties syntax.  Expects
056     * to be run by a GenericSecurityRealm (doesn't work on its own).
057     * <p/>
058     * This login module checks security credentials so the lifecycle methods must return true to indicate success
059     * or throw LoginException to indicate failure.
060     *
061     * @version $Rev: 565912 $ $Date: 2007-08-14 17:03:11 -0400 (Tue, 14 Aug 2007) $
062     */
063    public class PropertiesFileLoginModule implements LoginModule {
064        public final static String USERS_URI = "usersURI";
065        public final static String GROUPS_URI = "groupsURI";
066        public final static String DIGEST = "digest";
067        public final static String ENCODING = "encoding";
068    
069        private static Log log = LogFactory.getLog(PropertiesFileLoginModule.class);
070        final Properties users = new Properties();
071        final Map<String, Set<String>> groups = new HashMap<String, Set<String>>();
072        private String digest;
073        private String encoding;
074    
075        private Subject subject;
076        private CallbackHandler handler;
077        private String username;
078        private String password;
079    
080        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
081            this.subject = subject;
082            this.handler = callbackHandler;
083            try {
084                ServerInfo serverInfo = (ServerInfo) options.get(JaasLoginModuleUse.SERVERINFO_LM_OPTION);
085                final String users = (String) options.get(USERS_URI);
086                final String groups = (String) options.get(GROUPS_URI);
087                digest = (String) options.get(DIGEST);
088                encoding = (String) options.get(ENCODING);
089    
090                if (digest != null && !digest.equals("")) {
091                    // Check if the digest algorithm is available
092                    try {
093                        MessageDigest.getInstance(digest);
094                    } catch (NoSuchAlgorithmException e) {
095                        log.error("Initialization failed. Digest algorithm " + digest + " is not available.", e);
096                        throw new IllegalArgumentException(
097                                "Unable to configure properties file login module: " + e.getMessage(), e);
098                    }
099                    if (encoding != null && !"hex".equalsIgnoreCase(encoding) && !"base64".equalsIgnoreCase(encoding)) {
100                        log.error("Initialization failed. Digest Encoding " + encoding + " is not supported.");
101                        throw new IllegalArgumentException(
102                                "Unable to configure properties file login module. Digest Encoding " + encoding + " not supported.");
103                    }
104                }
105                if (users == null || groups == null) {
106                    throw new IllegalArgumentException("Both " + USERS_URI + " and " + GROUPS_URI + " must be provided!");
107                }
108                URI usersURI = new URI(users);
109                URI groupsURI = new URI(groups);
110                loadProperties(serverInfo, usersURI, groupsURI);
111            } catch (Exception e) {
112                log.error("Initialization failed", e);
113                throw new IllegalArgumentException("Unable to configure properties file login module: " + e.getMessage(),
114                        e);
115            }
116        }
117    
118        public void loadProperties(ServerInfo serverInfo, URI userURI, URI groupURI) throws GeronimoSecurityException {
119            try {
120                URI userFile = serverInfo.resolveServer(userURI);
121                URI groupFile = serverInfo.resolveServer(groupURI);
122                InputStream stream = userFile.toURL().openStream();
123                users.clear();
124                users.load(stream);
125                stream.close();
126    
127                Properties temp = new Properties();
128                stream = groupFile.toURL().openStream();
129                temp.load(stream);
130                stream.close();
131    
132                Enumeration e = temp.keys();
133                while (e.hasMoreElements()) {
134                    String groupName = (String) e.nextElement();
135                    String[] userList = ((String) temp.get(groupName)).split(",");
136    
137                    Set<String> userset = groups.get(groupName);
138                    if (userset == null) {
139                        userset = new HashSet<String>();
140                        groups.put(groupName, userset);
141                    }
142                    for (String user : userList) {
143                        userset.add(user);
144                    }
145                }
146    
147            } catch (Exception e) {
148                log.error("Properties File Login Module - data load failed", e);
149                throw new GeronimoSecurityException(e);
150            }
151        }
152    
153    
154        public boolean login() throws LoginException {
155            Callback[] callbacks = new Callback[2];
156    
157            callbacks[0] = new NameCallback("User name");
158            callbacks[1] = new PasswordCallback("Password", false);
159            try {
160                handler.handle(callbacks);
161            } catch (IOException ioe) {
162                throw (LoginException) new LoginException().initCause(ioe);
163            } catch (UnsupportedCallbackException uce) {
164                throw (LoginException) new LoginException().initCause(uce);
165            }
166            assert callbacks.length == 2;
167            username = ((NameCallback) callbacks[0]).getName();
168            if (username == null || username.equals("")) {
169                throw new FailedLoginException();
170            }
171            String realPassword = users.getProperty(username);
172            // Decrypt the password if needed, so we can compare it with the supplied one
173            if (realPassword != null) {
174                if (realPassword.startsWith("{Standard}")) {
175                    realPassword = (String) SimpleEncryption.decrypt(realPassword.substring(10));
176                }
177            }
178            char[] entered = ((PasswordCallback) callbacks[1]).getPassword();
179            password = entered == null ? null : new String(entered);
180            if (!checkPassword(realPassword, password)) {
181                throw new FailedLoginException();
182            }
183            return true;
184        }
185    
186        public boolean commit() throws LoginException {
187            Set<Principal> principals = subject.getPrincipals();
188    
189            principals.add(new GeronimoUserPrincipal(username));
190    
191            for (Map.Entry<String, Set<String>> entry : groups.entrySet()) {
192                String groupName = entry.getKey();
193                Set<String> users = entry.getValue();
194                for (String user : users) {
195                    if (username.equals(user)) {
196                        principals.add(new GeronimoGroupPrincipal(groupName));
197                        break;
198                    }
199                }
200            }
201    
202            return true;
203        }
204    
205        public boolean abort() throws LoginException {
206            username = null;
207            password = null;
208    
209            return true;
210        }
211    
212        public boolean logout() throws LoginException {
213            username = null;
214            password = null;
215            //todo: should remove principals added by commit
216            return true;
217        }
218    
219        /**
220         * This method checks if the provided password is correct.  The original password may have been digested.
221         *
222         * @param real     Original password in digested form if applicable
223         * @param provided User provided password in clear text
224         * @return true     If the password is correct
225         */
226        private boolean checkPassword(String real, String provided) {
227            if (real == null && provided == null) {
228                return true;
229            }
230            if (real == null || provided == null) {
231                return false;
232            }
233    
234            //both non-null
235            if (digest == null || digest.equals("")) {
236                // No digest algorithm is used
237                return real.equals(provided);
238            }
239            try {
240                // Digest the user provided password
241                MessageDigest md = MessageDigest.getInstance(digest);
242                byte[] data = md.digest(provided.getBytes());
243                if (encoding == null || "hex".equalsIgnoreCase(encoding)) {
244                    // Convert bytes to hex digits
245                    byte[] hexData = new byte[data.length * 2];
246                    HexTranslator ht = new HexTranslator();
247                    ht.encode(data, 0, data.length, hexData, 0);
248                    // Compare the digested provided password with the actual one
249                    return real.equalsIgnoreCase(new String(hexData));
250                } else if ("base64".equalsIgnoreCase(encoding)) {
251                    return real.equals(new String(Base64.encode(data)));
252                }
253            } catch (NoSuchAlgorithmException e) {
254                // Should not occur.  Availability of algorithm has been checked at initialization
255                log.error("Should not occur.  Availability of algorithm has been checked at initialization.", e);
256            }
257            return false;
258        }
259    }