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