001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020 package org.apache.directory.server.core.kerberos; 021 022 023 import org.apache.directory.server.core.interceptor.BaseInterceptor; 024 import org.apache.directory.server.core.interceptor.Interceptor; 025 import org.apache.directory.server.core.interceptor.NextInterceptor; 026 import org.apache.directory.server.core.interceptor.context.AddOperationContext; 027 import org.apache.directory.server.core.interceptor.context.ModifyOperationContext; 028 import org.apache.directory.shared.ldap.constants.SchemaConstants; 029 import org.apache.directory.shared.ldap.entry.BinaryValue; 030 import org.apache.directory.shared.ldap.entry.StringValue; 031 import org.apache.directory.shared.ldap.entry.EntryAttribute; 032 import org.apache.directory.shared.ldap.entry.Modification; 033 import org.apache.directory.shared.ldap.entry.ServerEntry; 034 import org.apache.directory.shared.ldap.entry.Value; 035 import org.apache.directory.shared.ldap.name.DN; 036 import org.apache.directory.shared.ldap.util.StringTools; 037 import org.slf4j.Logger; 038 import org.slf4j.LoggerFactory; 039 040 import java.util.ArrayList; 041 import java.util.List; 042 043 044 /** 045 * An {@link Interceptor} that enforces password policy for users. Add or modify operations 046 * on the 'userPassword' attribute are checked against a password policy. The password is 047 * rejected if it does not pass the password policy checks. The password MUST be passed to 048 * the core as plaintext. 049 * 050 * @org.apache.xbean.XBean 051 * 052 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 053 * @version $Rev$, $Date$ 054 */ 055 public class PasswordPolicyInterceptor extends BaseInterceptor 056 { 057 /** The log for this class. */ 058 private static final Logger log = LoggerFactory.getLogger( PasswordPolicyInterceptor.class ); 059 060 /** The service name. */ 061 public static final String NAME = "passwordPolicyService"; 062 063 064 /** 065 * Check added attributes for a 'userPassword'. If a 'userPassword' is found, apply any 066 * password policy checks. 067 */ 068 public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception 069 { 070 DN normName = addContext.getDn(); 071 072 ServerEntry entry = addContext.getEntry(); 073 074 log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getName() ); 075 076 if ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) 077 { 078 String username = null; 079 080 BinaryValue userPassword = (BinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get(); 081 082 // The password is stored in a non H/R attribute, but it's a String 083 String strUserPassword = userPassword.getString(); 084 085 if ( log.isDebugEnabled() ) 086 { 087 StringBuffer sb = new StringBuffer(); 088 sb.append( "'" + strUserPassword + "' ( " ); 089 sb.append( userPassword ); 090 sb.append( " )" ); 091 log.debug( "Adding Attribute id : 'userPassword', Values : [ {} ]", sb.toString() ); 092 } 093 094 if ( entry.get( SchemaConstants.CN_AT ) != null ) 095 { 096 StringValue attr = (StringValue)entry.get( SchemaConstants.CN_AT ).get(); 097 username = attr.getString(); 098 } 099 100 // If userPassword fails checks, throw new NamingException. 101 check( username, strUserPassword ); 102 } 103 104 next.add( addContext ); 105 } 106 107 108 /** 109 * Check modification items for a 'userPassword'. If a 'userPassword' is found, apply any 110 * password policy checks. 111 */ 112 public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception 113 { 114 DN name = modContext.getDn(); 115 116 List<Modification> mods = modContext.getModItems(); 117 118 String operation = null; 119 120 for ( Modification mod:mods ) 121 { 122 if ( log.isDebugEnabled() ) 123 { 124 switch ( mod.getOperation() ) 125 { 126 case ADD_ATTRIBUTE: 127 operation = "Adding"; 128 break; 129 130 case REMOVE_ATTRIBUTE: 131 operation = "Removing"; 132 break; 133 134 case REPLACE_ATTRIBUTE: 135 operation = "Replacing"; 136 break; 137 } 138 } 139 140 EntryAttribute attr = mod.getAttribute(); 141 142 if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) ) 143 { 144 Value<?> userPassword = attr.get(); 145 String pwd = ""; 146 147 if ( userPassword != null ) 148 { 149 if ( userPassword instanceof StringValue ) 150 { 151 log.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, attr ); 152 pwd = ((StringValue)userPassword).getString(); 153 } 154 else if ( userPassword instanceof BinaryValue ) 155 { 156 BinaryValue password = (BinaryValue)userPassword; 157 158 String string = password.getString(); 159 160 if ( log.isDebugEnabled() ) 161 { 162 StringBuffer sb = new StringBuffer(); 163 sb.append( "'" + string + "' ( " ); 164 sb.append( StringTools.dumpBytes( password.getBytes() ).trim() ); 165 sb.append( " )" ); 166 log.debug( "{} Attribute id : 'userPassword', Values : [ {} ]", operation, sb.toString() ); 167 } 168 169 pwd = string; 170 } 171 172 // if userPassword fails checks, throw new NamingException. 173 check( name.getName(), pwd ); 174 } 175 } 176 177 if ( log.isDebugEnabled() ) 178 { 179 log.debug( operation + " for entry '" + name.getName() + "' the attribute " + mod.getAttribute() ); 180 } 181 } 182 183 next.modify( modContext ); 184 } 185 186 187 void check( String username, String password ) throws Exception 188 { 189 int passwordLength = 6; 190 int categoryCount = 2; 191 int tokenSize = 3; 192 193 if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) ) 194 { 195 String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize ); 196 log.error( explanation ); 197 198 throw new Exception( explanation ); 199 } 200 } 201 202 203 /** 204 * Tests that: 205 * The password is at least six characters long. 206 * The password contains a mix of characters. 207 * The password does not contain three letter (or more) tokens from the user's account name. 208 */ 209 boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize ) 210 { 211 return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount ) 212 && isValidUsernameSubstring( username, password, tokenSize ); 213 } 214 215 216 /** 217 * The password is at least six characters long. 218 */ 219 boolean isValidPasswordLength( String password, int passwordLength ) 220 { 221 return password.length() >= passwordLength; 222 } 223 224 225 /** 226 * The password contains characters from at least three of the following four categories: 227 * English uppercase characters (A - Z) 228 * English lowercase characters (a - z) 229 * Base 10 digits (0 - 9) 230 * Any non-alphanumeric character (for example: !, $, #, or %) 231 */ 232 boolean isValidCategoryCount( String password, int categoryCount ) 233 { 234 int uppercase = 0; 235 int lowercase = 0; 236 int digit = 0; 237 int nonAlphaNumeric = 0; 238 239 char[] characters = password.toCharArray(); 240 241 for ( char character:characters ) 242 { 243 if ( Character.isLowerCase( character ) ) 244 { 245 lowercase = 1; 246 } 247 else 248 { 249 if ( Character.isUpperCase( character ) ) 250 { 251 uppercase = 1; 252 } 253 else 254 { 255 if ( Character.isDigit( character ) ) 256 { 257 digit = 1; 258 } 259 else 260 { 261 if ( !Character.isLetterOrDigit( character ) ) 262 { 263 nonAlphaNumeric = 1; 264 } 265 } 266 } 267 } 268 } 269 270 return ( uppercase + lowercase + digit + nonAlphaNumeric ) >= categoryCount; 271 } 272 273 274 /** 275 * The password does not contain three letter (or more) tokens from the user's account name. 276 * 277 * If the account name is less than three characters long, this check is not performed 278 * because the rate at which passwords would be rejected is too high. For each token that is 279 * three or more characters long, that token is searched for in the password; if it is present, 280 * the password change is rejected. For example, the name "First M. Last" would be split into 281 * three tokens: "First", "M", and "Last". Because the second token is only one character long, 282 * it would be ignored. Therefore, this user could not have a password that included either 283 * "first" or "last" as a substring anywhere in the password. All of these checks are 284 * case-insensitive. 285 */ 286 boolean isValidUsernameSubstring( String username, String password, int tokenSize ) 287 { 288 String[] tokens = username.split( "[^a-zA-Z]" ); 289 290 for ( int ii = 0; ii < tokens.length; ii++ ) 291 { 292 if ( tokens[ii].length() >= tokenSize ) 293 { 294 if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) ) 295 { 296 return false; 297 } 298 } 299 } 300 301 return true; 302 } 303 304 305 private String buildErrorMessage( String username, String password, int passwordLength, int categoryCount, 306 int tokenSize ) 307 { 308 List<String> violations = new ArrayList<String>(); 309 310 if ( !isValidPasswordLength( password, passwordLength ) ) 311 { 312 violations.add( "length too short" ); 313 } 314 315 if ( !isValidCategoryCount( password, categoryCount ) ) 316 { 317 violations.add( "insufficient character mix" ); 318 } 319 320 if ( !isValidUsernameSubstring( username, password, tokenSize ) ) 321 { 322 violations.add( "contains portions of username" ); 323 } 324 325 StringBuffer sb = new StringBuffer( "Password violates policy: " ); 326 327 boolean isFirst = true; 328 329 for ( String violation : violations ) 330 { 331 if ( isFirst ) 332 { 333 isFirst = false; 334 } 335 else 336 { 337 sb.append( ", " ); 338 } 339 340 sb.append( violation ); 341 } 342 343 return sb.toString(); 344 } 345 }