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