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: */package org.apache.geronimo.security.realm.providers;
017:
018: import java.io.IOException;
019: import java.io.InputStream;
020: import java.net.URI;
021: import java.security.MessageDigest;
022: import java.security.NoSuchAlgorithmException;
023: import java.security.Principal;
024: import java.util.Arrays;
025: import java.util.Collections;
026: import java.util.Enumeration;
027: import java.util.HashMap;
028: import java.util.HashSet;
029: import java.util.List;
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:
044: import org.apache.commons.logging.Log;
045: import org.apache.commons.logging.LogFactory;
046: import org.apache.geronimo.common.GeronimoSecurityException;
047: import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
048: import org.apache.geronimo.security.jaas.WrappingLoginModule;
049: import org.apache.geronimo.system.serverinfo.ServerInfo;
050: import org.apache.geronimo.crypto.SimpleEncryption;
051: import org.apache.geronimo.crypto.EncryptionManager;
052: import org.apache.geronimo.crypto.encoders.Base64;
053: import org.apache.geronimo.crypto.encoders.HexTranslator;
054:
055: /**
056: * A LoginModule that reads a list of credentials and group from files on disk. The
057: * files should be formatted using standard Java properties syntax. Expects
058: * to be run by a GenericSecurityRealm (doesn't work on its own).
059: * <p/>
060: * This login module checks security credentials so the lifecycle methods must return true to indicate success
061: * or throw LoginException to indicate failure.
062: *
063: * @version $Rev: 617588 $ $Date: 2008-02-01 10:20:07 -0800 (Fri, 01 Feb 2008) $
064: */
065: public class PropertiesFileLoginModule implements LoginModule {
066: public final static String USERS_URI = "usersURI";
067: public final static String GROUPS_URI = "groupsURI";
068: public final static String DIGEST = "digest";
069: public final static String ENCODING = "encoding";
070: public final static List<String> supportedOptions = Collections
071: .unmodifiableList(Arrays.asList(USERS_URI, GROUPS_URI,
072: DIGEST, ENCODING));
073:
074: private static Log log = LogFactory
075: .getLog(PropertiesFileLoginModule.class);
076: final Properties users = new Properties();
077: final Map<String, Set<String>> groups = new HashMap<String, Set<String>>();
078: private String digest;
079: private String encoding;
080:
081: private boolean loginSucceeded;
082: private Subject subject;
083: private CallbackHandler handler;
084: private String username;
085: private String password;
086: private final Set<Principal> allPrincipals = new HashSet<Principal>();
087:
088: public void initialize(Subject subject,
089: CallbackHandler callbackHandler, Map sharedState,
090: Map options) {
091: this .subject = subject;
092: this .handler = callbackHandler;
093: for (Object option : options.keySet()) {
094: if (!supportedOptions.contains(option)
095: && !JaasLoginModuleUse.supportedOptions
096: .contains(option)
097: && !WrappingLoginModule.supportedOptions
098: .contains(option)) {
099: log.warn("Ignoring option: " + option
100: + ". Not supported.");
101: }
102: }
103: try {
104: ServerInfo serverInfo = (ServerInfo) options
105: .get(JaasLoginModuleUse.SERVERINFO_LM_OPTION);
106: final String users = (String) options.get(USERS_URI);
107: final String groups = (String) options.get(GROUPS_URI);
108: digest = (String) options.get(DIGEST);
109: encoding = (String) options.get(ENCODING);
110:
111: if (digest != null && !digest.equals("")) {
112: // Check if the digest algorithm is available
113: try {
114: MessageDigest.getInstance(digest);
115: } catch (NoSuchAlgorithmException e) {
116: log.error(
117: "Initialization failed. Digest algorithm "
118: + digest + " is not available.", e);
119: throw new IllegalArgumentException(
120: "Unable to configure properties file login module: "
121: + e.getMessage(), e);
122: }
123: if (encoding != null
124: && !"hex".equalsIgnoreCase(encoding)
125: && !"base64".equalsIgnoreCase(encoding)) {
126: log.error("Initialization failed. Digest Encoding "
127: + encoding + " is not supported.");
128: throw new IllegalArgumentException(
129: "Unable to configure properties file login module. Digest Encoding "
130: + encoding + " not supported.");
131: }
132: }
133: if (users == null || groups == null) {
134: throw new IllegalArgumentException("Both " + USERS_URI
135: + " and " + GROUPS_URI + " must be provided!");
136: }
137: URI usersURI = new URI(users);
138: URI groupsURI = new URI(groups);
139: loadProperties(serverInfo, usersURI, groupsURI);
140: } catch (Exception e) {
141: log.error("Initialization failed", e);
142: throw new IllegalArgumentException(
143: "Unable to configure properties file login module: "
144: + e.getMessage(), e);
145: }
146: }
147:
148: public void loadProperties(ServerInfo serverInfo, URI userURI,
149: URI groupURI) throws GeronimoSecurityException {
150: try {
151: URI userFile = serverInfo.resolveServer(userURI);
152: URI groupFile = serverInfo.resolveServer(groupURI);
153: InputStream stream = userFile.toURL().openStream();
154: users.clear();
155: users.load(stream);
156: stream.close();
157:
158: Properties temp = new Properties();
159: stream = groupFile.toURL().openStream();
160: temp.load(stream);
161: stream.close();
162:
163: Enumeration e = temp.keys();
164: while (e.hasMoreElements()) {
165: String groupName = (String) e.nextElement();
166: String[] userList = ((String) temp.get(groupName))
167: .split(",");
168:
169: Set<String> userset = groups.get(groupName);
170: if (userset == null) {
171: userset = new HashSet<String>();
172: groups.put(groupName, userset);
173: }
174: for (String user : userList) {
175: userset.add(user);
176: }
177: }
178:
179: } catch (Exception e) {
180: log.error(
181: "Properties File Login Module - data load failed",
182: e);
183: throw new GeronimoSecurityException(e);
184: }
185: }
186:
187: /**
188: * This LoginModule is not to be ignored. So, this method should never return false.
189: * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
190: * if authentication fails
191: */
192: public boolean login() throws LoginException {
193: loginSucceeded = false;
194: Callback[] callbacks = new Callback[2];
195:
196: callbacks[0] = new NameCallback("User name");
197: callbacks[1] = new PasswordCallback("Password", false);
198: try {
199: handler.handle(callbacks);
200: } catch (IOException ioe) {
201: throw (LoginException) new LoginException().initCause(ioe);
202: } catch (UnsupportedCallbackException uce) {
203: throw (LoginException) new LoginException().initCause(uce);
204: }
205: assert callbacks.length == 2;
206: username = ((NameCallback) callbacks[0]).getName();
207: if (username == null || username.equals("")) {
208: // Clear out the private state
209: username = null;
210: password = null;
211: throw new FailedLoginException();
212: }
213: String realPassword = users.getProperty(username);
214: // Decrypt the password if needed, so we can compare it with the supplied one
215: if (realPassword != null) {
216: realPassword = (String) EncryptionManager
217: .decrypt(realPassword);
218: }
219: char[] entered = ((PasswordCallback) callbacks[1])
220: .getPassword();
221: password = entered == null ? null : new String(entered);
222: if (!checkPassword(realPassword, password)) {
223: // Clear out the private state
224: username = null;
225: password = null;
226: throw new FailedLoginException();
227: }
228:
229: loginSucceeded = true;
230: return true;
231: }
232:
233: /*
234: * @exception LoginException if login succeeded but commit failed.
235: *
236: * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
237: */
238: public boolean commit() throws LoginException {
239: if (loginSucceeded) {
240: if (username != null) {
241: allPrincipals.add(new GeronimoUserPrincipal(username));
242: }
243: for (Map.Entry<String, Set<String>> entry : groups
244: .entrySet()) {
245: String groupName = entry.getKey();
246: Set<String> users = entry.getValue();
247: for (String user : users) {
248: if (username.equals(user)) {
249: allPrincipals.add(new GeronimoGroupPrincipal(
250: groupName));
251: break;
252: }
253: }
254: }
255: subject.getPrincipals().addAll(allPrincipals);
256: }
257: // Clear out the private state
258: username = null;
259: password = null;
260:
261: return loginSucceeded;
262: }
263:
264: public boolean abort() throws LoginException {
265: if (loginSucceeded) {
266: // Clear out the private state
267: username = null;
268: password = null;
269: allPrincipals.clear();
270: }
271: return loginSucceeded;
272: }
273:
274: public boolean logout() throws LoginException {
275: // Clear out the private state
276: loginSucceeded = false;
277: username = null;
278: password = null;
279: if (!subject.isReadOnly()) {
280: // Remove principals added by this LoginModule
281: subject.getPrincipals().removeAll(allPrincipals);
282: }
283: allPrincipals.clear();
284: return true;
285: }
286:
287: /**
288: * This method checks if the provided password is correct. The original password may have been digested.
289: *
290: * @param real Original password in digested form if applicable
291: * @param provided User provided password in clear text
292: * @return true If the password is correct
293: */
294: private boolean checkPassword(String real, String provided) {
295: if (real == null && provided == null) {
296: return true;
297: }
298: if (real == null || provided == null) {
299: return false;
300: }
301:
302: //both non-null
303: if (digest == null || digest.equals("")) {
304: // No digest algorithm is used
305: return real.equals(provided);
306: }
307: try {
308: // Digest the user provided password
309: MessageDigest md = MessageDigest.getInstance(digest);
310: byte[] data = md.digest(provided.getBytes());
311: if (encoding == null || "hex".equalsIgnoreCase(encoding)) {
312: // Convert bytes to hex digits
313: byte[] hexData = new byte[data.length * 2];
314: HexTranslator ht = new HexTranslator();
315: ht.encode(data, 0, data.length, hexData, 0);
316: // Compare the digested provided password with the actual one
317: return real.equalsIgnoreCase(new String(hexData));
318: } else if ("base64".equalsIgnoreCase(encoding)) {
319: return real.equals(new String(Base64.encode(data)));
320: }
321: } catch (NoSuchAlgorithmException e) {
322: // Should not occur. Availability of algorithm has been checked at initialization
323: log
324: .error(
325: "Should not occur. Availability of algorithm has been checked at initialization.",
326: e);
327: }
328: return false;
329: }
330: }
|