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 }