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 }