You are viewing a plain text version of this content. The canonical link for it is here.
Posted to derby-commits@db.apache.org by rh...@apache.org on 2012/01/31 19:14:41 UTC

svn commit: r1238727 - in /db/derby/code/trunk/java: engine/org/apache/derby/iapi/reference/ engine/org/apache/derby/impl/jdbc/ engine/org/apache/derby/impl/jdbc/authentication/ engine/org/apache/derby/loc/ shared/org/apache/derby/shared/common/referen...

Author: rhillegas
Date: Tue Jan 31 18:14:41 2012
New Revision: 1238727

URL: http://svn.apache.org/viewvc?rev=1238727&view=rev
Log:
DERBY-866: Add expiration limits for NATIVE passwords.

Modified:
    db/derby/code/trunk/java/engine/org/apache/derby/iapi/reference/Property.java
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/EmbedConnection.java
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/AuthenticationServiceBase.java
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/NativeAuthenticationServiceImpl.java
    db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml
    db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
    db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/NativeAuthenticationServiceTest.java

Modified: db/derby/code/trunk/java/engine/org/apache/derby/iapi/reference/Property.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/iapi/reference/Property.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/iapi/reference/Property.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/iapi/reference/Property.java Tue Jan 31 18:14:41 2012
@@ -818,6 +818,21 @@ public interface Property { 
     public static final String AUTHENTICATION_PROVIDER_NATIVE_LOCAL =
         AUTHENTICATION_PROVIDER_NATIVE + AUTHENTICATION_PROVIDER_LOCAL_SUFFIX;
     
+    // lifetime (in milliseconds) of a NATIVE password. if <= 0, then the password never expires
+    public static final String AUTHENTICATION_NATIVE_PASSWORD_LIFETIME =
+        "derby.authentication.native.passwordLifetimeMillis";
+    
+    // default lifetime (in milliseconds) of a NATIVE password. 31 days.
+    public static final long MILLISECONDS_IN_DAY = 1000L * 60L * 60L * 24L;
+    public static final long AUTHENTICATION_NATIVE_PASSWORD_LIFETIME_DEFAULT = MILLISECONDS_IN_DAY * 31L;
+    
+    // threshhold for raising a warning that a password is about to expire.
+    // raise a warning if the remaining password lifetime is less than this proportion of the max lifetime.
+    public static final String  AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD =
+        "derby.authentication.native.passwordLifetimeThreshold";
+    public static final double  AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD_DEFAULT = 0.125;
+    
+
     /**
      * Property that specifies the name of the hash algorithm to use with
      * the configurable hash authentication scheme.

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/EmbedConnection.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/EmbedConnection.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/EmbedConnection.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/EmbedConnection.java Tue Jan 31 18:14:41 2012
@@ -1240,15 +1240,24 @@ public abstract class EmbedConnection im
 		}
 
 		// Let's authenticate now
-			
-		if (!authenticationService.authenticate(
-											   dbname,
-											   userInfo
-											   )) {
 
+        boolean authenticationSucceeded = true;
+
+        try {
+            authenticationSucceeded = authenticationService.authenticate( dbname, userInfo );
+        }
+        catch (SQLWarning warnings)
+        {
+            //
+            // Let the user handle the warning that her password is about to expire.
+            //
+            addWarning( warnings );
+        }
+			
+		if ( !authenticationSucceeded )
+        {
 			throw newSQLException(SQLState.NET_CONNECT_AUTH_FAILED,
                      MessageService.getTextMessage(MessageId.AUTH_INVALID));
-
 		}
 
 		// If authentication is not on, we have to raise a warning if sqlAuthorization is ON

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/AuthenticationServiceBase.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/AuthenticationServiceBase.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/AuthenticationServiceBase.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/AuthenticationServiceBase.java Tue Jan 31 18:14:41 2012
@@ -407,12 +407,51 @@ public abstract class AuthenticationServ
             }
         }
 
+        if ( Property.AUTHENTICATION_NATIVE_PASSWORD_LIFETIME.equals( key ) )
+        {
+            if ( parsePasswordLifetime( stringValue ) == null )
+            {
+                throw StandardException.newException
+                    ( SQLState.BAD_PASSWORD_LIFETIME, Property.AUTHENTICATION_NATIVE_PASSWORD_LIFETIME );
+            }
+        }
+        
+        if ( Property.AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD.equals( key ) )
+        {
+            if ( parsePasswordThreshold( stringValue ) == null )
+            {
+                throw StandardException.newException
+                    ( SQLState.BAD_PASSWORD_LIFETIME, Property.AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD );
+            }
+        }
+        
         return false;
 	}
     private StandardException   badNativeAuthenticationChange()
     {
         return StandardException.newException( SQLState.PROPERTY_BAD_NATIVE_CHANGE );
     }
+    /** Parse the value of the password lifetime property. Return null if it is bad. */
+    protected   Long    parsePasswordLifetime( String passwordLifetimeString )
+    {
+            try {
+                long    passwordLifetime = Long.parseLong( passwordLifetimeString );
+
+                if ( passwordLifetime < 0L ) { passwordLifetime = 0L; }
+
+                return new Long( passwordLifetime );
+            } catch (Exception e) { return null; }
+    }
+    /** Parse the value of the password expiration threshold property. Return null if it is bad. */
+    protected   Double  parsePasswordThreshold( String expirationThresholdString )
+    {
+            try {
+                double  expirationThreshold = Double.parseDouble( expirationThresholdString );
+
+                if ( expirationThreshold <= 0L ) { return null; }
+                else { return new Double( expirationThreshold ); }
+            } catch (Exception e) { return null; }
+    }
     
 	/**
 	  @see PropertySetCallback#validate

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/NativeAuthenticationServiceImpl.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/NativeAuthenticationServiceImpl.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/NativeAuthenticationServiceImpl.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/authentication/NativeAuthenticationServiceImpl.java Tue Jan 31 18:14:41 2012
@@ -26,6 +26,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.sql.Connection;
 import java.sql.SQLException;
+import java.sql.SQLWarning;
 import java.util.Arrays;
 import javax.sql.DataSource;
 
@@ -39,6 +40,7 @@ import org.apache.derby.iapi.sql.diction
 import org.apache.derby.iapi.reference.Attribute;
 import org.apache.derby.iapi.reference.SQLState;
 import org.apache.derby.authentication.UserAuthenticator;
+import org.apache.derby.iapi.error.SQLWarningFactory;
 import org.apache.derby.iapi.error.StandardException;
 import org.apache.derby.iapi.services.monitor.Monitor;
 import org.apache.derby.iapi.services.property.PropertyUtil;
@@ -87,6 +89,9 @@ public final class NativeAuthenticationS
     
     private String      _credentialsDB;
     private boolean _authenticateDatabaseOperationsLocally;
+    private long        _passwordLifetimeMillis = Property.AUTHENTICATION_NATIVE_PASSWORD_LIFETIME_DEFAULT;
+    private double      _passwordExpirationThreshold = Property.AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD_DEFAULT;
+    private String      _badlyFormattedPasswordProperty;
 
     ///////////////////////////////////////////////////////////////////////////////////
     //
@@ -149,6 +154,39 @@ public final class NativeAuthenticationS
 
             if ( _credentialsDB.length() == 0 ) { _credentialsDB = null; }
         }
+
+        //
+        // Let the application override password lifespans.
+        //
+        _badlyFormattedPasswordProperty = null;
+        String passwordLifetimeString = PropertyUtil.getPropertyFromSet
+            (
+             properties,
+             Property.AUTHENTICATION_NATIVE_PASSWORD_LIFETIME
+             );
+        if ( passwordLifetimeString != null )
+        {
+            Long    passwordLifetime = parsePasswordLifetime( passwordLifetimeString );
+
+            if ( passwordLifetime != null ) { _passwordLifetimeMillis = passwordLifetime.longValue(); }
+            else
+            { _badlyFormattedPasswordProperty = Property.AUTHENTICATION_NATIVE_PASSWORD_LIFETIME; }
+        }
+
+        String  expirationThresholdString = PropertyUtil.getPropertyFromSet
+            (
+             properties,
+             Property.AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD
+             );
+        if ( expirationThresholdString != null )
+        {
+            Double  expirationThreshold = parsePasswordThreshold( expirationThresholdString );
+
+            if ( expirationThreshold != null ) { _passwordExpirationThreshold = expirationThreshold.doubleValue(); }
+            else
+            { _badlyFormattedPasswordProperty = Property.AUTHENTICATION_PASSWORD_EXPIRATION_THRESHOLD; }
+        }
+        
     }
 
     /**
@@ -178,6 +216,12 @@ public final class NativeAuthenticationS
             throw StandardException.newException( SQLState.BAD_NATIVE_AUTH_SPEC );
         }
 
+        if ( _badlyFormattedPasswordProperty != null )
+        {
+            throw StandardException.newException
+                ( SQLState.BAD_PASSWORD_LIFETIME, _badlyFormattedPasswordProperty );
+        }
+
 		// Initialize the MessageDigest class engine here
 		// (we don't need to do that ideally, but there is some
 		// overhead the first time it is instantiated.
@@ -340,7 +384,7 @@ public final class NativeAuthenticationS
          String userPassword,
          String databaseName
          )
-        throws StandardException
+        throws StandardException, SQLWarning
 	{
         // this catches the case when someone specifies derby.authentication.provider=NATIVE::LOCAL
         // at the system level
@@ -353,6 +397,8 @@ public final class NativeAuthenticationS
             "org.apache.derby.jdbc.EmbeddedSimpleDataSource" :
             "org.apache.derby.jdbc.EmbeddedDataSource";
 
+        SQLWarning  warnings = null;
+        
         try {
             DataSource  dataSource = (DataSource) Class.forName( dataSourceName ).newInstance();
 
@@ -361,6 +407,8 @@ public final class NativeAuthenticationS
             callDataSourceSetter( dataSource, "setPassword", userPassword );
 
             Connection  conn = dataSource.getConnection();
+
+            warnings = conn.getWarnings();
             conn.close();
         }
         catch (ClassNotFoundException cnfe) { throw wrap( cnfe ); }
@@ -378,6 +426,10 @@ public final class NativeAuthenticationS
             else { throw wrap( se ); }
         }
 
+        // let warnings percolate up so that EmbedConnection can handle notifications
+        // about expiring passwords
+        if ( warnings != null ) { throw warnings; }
+
         // If we get here, then we successfully connected to the credentials database. Hooray.
         return true;
     }
@@ -445,17 +497,48 @@ public final class NativeAuthenticationS
         PasswordHasher      hasher = new PasswordHasher( userDescriptor.getHashingScheme() );
         char[]                     candidatePassword = hasher.hashPasswordIntoString( userName, userPassword ).toCharArray();
         char[]                     actualPassword = userDescriptor.getAndZeroPassword();
+
+        try {
+            if ( (candidatePassword == null) || (actualPassword == null)) { return false; }
+            if ( candidatePassword.length != actualPassword.length ) { return false; }
         
-        if ( (candidatePassword == null) || (actualPassword == null)) { return false; }
-        if ( candidatePassword.length != actualPassword.length ) { return false; }
-        
-        for ( int i = 0; i < candidatePassword.length; i++ )
+            for ( int i = 0; i < candidatePassword.length; i++ )
+            {
+                if ( candidatePassword[ i ] != actualPassword[ i ] ) { return false; }
+            }
+        } finally
         {
-            if ( candidatePassword[ i ] != actualPassword[ i ] ) { return false; }
+            Arrays.fill( candidatePassword, (char) 0 );
+            Arrays.fill( actualPassword, (char) 0 );
+        }
+
+        //
+        // Password is good. Check whether the password has expired or will expire soon.
+        //
+        if ( _passwordLifetimeMillis > 0 )
+        {
+            long    passwordAge = System.currentTimeMillis() - userDescriptor.getLastModified().getTime();
+            long    remainingLifetime = _passwordLifetimeMillis - passwordAge;
+
+            //
+            // Oops, the password has expired. Fail the authentication. Say nothing more
+            // so that we give password crackers as little information as possible.
+            //
+            if ( remainingLifetime <= 0L )
+            {
+                // The DBO's password never expires.
+                if ( !dd.getAuthorizationDatabaseOwner().equals( userName ) ) { return false; }
+                else { remainingLifetime = 0L; }
+            }
+
+            long    expirationThreshold = (long) ( _passwordLifetimeMillis * _passwordExpirationThreshold );
+            
+            if ( remainingLifetime <= expirationThreshold )
+            {
+                long    daysRemaining = remainingLifetime / Property.MILLISECONDS_IN_DAY;
+                throw SQLWarningFactory.newSQLWarning( SQLState.PASSWORD_EXPIRES_SOON, Long.toString( daysRemaining ) );
+            }
         }
-        
-        Arrays.fill( candidatePassword, (char) 0 );
-        Arrays.fill( actualPassword, (char) 0 );
         
         return true;
     }

Modified: db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml Tue Jan 31 18:14:41 2012
@@ -211,6 +211,12 @@ Guide.
                 <text>SQL authorization is being used without first enabling authentication.</text>
             </msg>
 
+            <msg>
+                <name>01J15</name>
+                <text>Your password will expire in {0} day(s). Please use the SYSCS_UTIL.SYSCS_MODIFY_PASSWORD  procedure to change your password.</text>
+                <arg>remainingDays</arg>
+            </msg>
+
         </family>
 
         <family>
@@ -1250,6 +1256,12 @@ Guide.
            </msg>
 
             <msg>
+                <name>4251J</name>
+                <text>The value for the property '{0}' is formatted badly.</text>
+                 <arg>propertyName</arg>
+           </msg>
+
+            <msg>
                 <name>42601</name>
                 <text>In an ALTER TABLE statement, the column '{0}' has been specified as NOT NULL and either the DEFAULT clause was not specified or was specified as DEFAULT NULL.</text>
                 <arg>columnName</arg>

Modified: db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java (original)
+++ db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java Tue Jan 31 18:14:41 2012
@@ -783,6 +783,7 @@ public interface SQLState {
 	String WEAK_AUTHENTICATION                                               = "4251G";
 	String BAD_NATIVE_AUTH_SPEC                                               = "4251H";
 	String MISSING_CREDENTIALS_DB                                               = "4251I";
+	String BAD_PASSWORD_LIFETIME                                               = "4251J";
 
 	String LANG_DB2_NOT_NULL_COLUMN_INVALID_DEFAULT                    = "42601";
 	String LANG_DB2_INVALID_HEXADECIMAL_CONSTANT                    = "42606";
@@ -1742,6 +1743,7 @@ public interface SQLState {
     String UNABLE_TO_OBTAIN_MESSAGE_TEXT_FROM_SERVER  = "01J12";
     String NUMBER_OF_ROWS_TOO_LARGE_FOR_INT = "01J13";
 	String SQL_AUTHORIZATION_WITH_NO_AUTHENTICATION = "01J14";
+	String PASSWORD_EXPIRES_SOON = "01J15";
 		
     String CURSOR_OPERATION_CONFLICT = "01001";
 

Modified: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/NativeAuthenticationServiceTest.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/NativeAuthenticationServiceTest.java?rev=1238727&r1=1238726&r2=1238727&view=diff
==============================================================================
--- db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/NativeAuthenticationServiceTest.java (original)
+++ db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/NativeAuthenticationServiceTest.java Tue Jan 31 18:14:41 2012
@@ -23,6 +23,7 @@ package org.apache.derbyTesting.function
 
 import java.sql.Connection;
 import java.sql.SQLException;
+import java.sql.SQLWarning;
 import java.util.Properties;
 
 import junit.extensions.TestSetup;
@@ -58,6 +59,7 @@ public class NativeAuthenticationService
     private static  final   String  SECOND_DB = "secondDB";
     private static  final   String  THIRD_DB = "thirdDB";
     private static  final   String  FOURTH_DB = "fourthDB";
+    private static  final   String  FIFTH_DB = "fifthDB";
 
     private static  final   String  PROVIDER_PROPERTY = "derby.authentication.provider";
 
@@ -67,6 +69,8 @@ public class NativeAuthenticationService
     private static  final   String  INVALID_PROVIDER_CHANGE = "XCY05";
     private static  final   String  CANT_DROP_DBO = "4251F";
     private static  final   String  NO_COLUMN_PERMISSION = "42502";
+    private static  final   String  PASSWORD_EXPIRING = "01J15";
+    private static  final   String  BAD_PASSWORD_PROPERTY = "4251J";
 
     ///////////////////////////////////////////////////////////////////////////////////
     //
@@ -78,6 +82,7 @@ public class NativeAuthenticationService
     private final   boolean _localAuthentication;
 
     private DatabaseChangeSetup _fourthDBSetup;
+    private DatabaseChangeSetup _fifthDBSetup;
 
     ///////////////////////////////////////////////////////////////////////////////////
     //
@@ -132,11 +137,17 @@ public class NativeAuthenticationService
             "NATIVE authentication on, " :
             "Authentication off, ";
         String  local = _localAuthentication ?
-            "LOCAL authentication ON" :
-            "LOCAL authentication OFF";
+            "LOCAL authentication ON, " :
+            "LOCAL authentication OFF, ";
+        String  embedded = isEmbedded() ?
+            "Embedded" :
+            "Client/Server";
 
-        return "[ " + authType + local + " ]";
+        return "[ " + authType + local + embedded + " ]";
     }
+
+    /** Return true if the test is running embedded */
+    public  boolean isEmbedded() { return getTestConfiguration().getJDBCClient().isEmbedded(); }
     
     ///////////////////////////////////////////////////////////////////////////////////
     //
@@ -228,6 +239,7 @@ public class NativeAuthenticationService
         result = TestConfiguration.additionalDatabaseDecoratorNoShutdown( result, SECOND_DB );
         result = TestConfiguration.additionalDatabaseDecoratorNoShutdown( result, THIRD_DB );
         result = _fourthDBSetup = TestConfiguration.additionalDatabaseDecoratorNoShutdown( result, FOURTH_DB, true );
+        result = _fifthDBSetup = TestConfiguration.additionalDatabaseDecoratorNoShutdown( result, FIFTH_DB, true );
 
         result = TestConfiguration.changeUserDecorator( result, DBO, getPassword( DBO ) );
         
@@ -249,17 +261,23 @@ public class NativeAuthenticationService
     {
         println( nameOfTest() );
 
-        vetForAllConfigurations();
+        vetCoreBehavior();
+
+        if ( !_nativeAuthentication ) { vetProviderChanges(); }
 
-        if ( !_nativeAuthentication ) { vetUnauthenticatedConfiguration(); }
+        // only run this for local authentication so that we don't have to shutdown
+        // the system-wide credentials db. also only run this embedded so that we
+        // don't have to deal with the problems of shutting down a database
+        // across the network.
+        if ( _localAuthentication && isEmbedded() ) { vetPasswordLifetime(); }
     }
 
     /**
      * <p>
-     * These tests are run for all configurations.
+     * Verify the core behavior of NATIVE authentication.
      * </p>
      */
-    private void    vetForAllConfigurations()   throws Exception
+    private void    vetCoreBehavior()   throws Exception
     {
         // can't create any database until the credentials db has been created
         Connection  secondDBConn = getConnection
@@ -370,10 +388,11 @@ public class NativeAuthenticationService
 
     /**
      * <p>
+     * Try changing the value of the provider property on disk.
      * These tests are run only if authentication is turned off.
      * </p>
      */
-    private void    vetUnauthenticatedConfiguration()   throws Exception
+    private void    vetProviderChanges()   throws Exception
     {
         // create an empty database without authentication turned on
         String          dbo = ORANGE_USER;
@@ -471,6 +490,68 @@ public class NativeAuthenticationService
         
     }
     
+    /**
+     * <p>
+     * Verify that password lifetimes are checked.
+     * </p>
+     */
+    private void    vetPasswordLifetime()   throws Exception
+    {
+        // create another database
+        Connection  dboConn = openConnection( FIFTH_DB, DBO );
+
+        // add another legal user
+        addUser( dboConn, APPLE_USER );
+
+        Connection  appleConn = passwordExpiring( false, FIFTH_DB, APPLE_USER );
+
+        // setup so that passwords are expiring after the db is rebooted.
+        // shutdown the database in this test so that the new property settings take effect.
+        goodStatement
+            ( dboConn, "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeMillis', '86400000' )" );
+        goodStatement
+            ( dboConn, "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeThreshold', '2.0' )" );
+        _fifthDBSetup.getTestConfiguration().shutdownDatabase();
+ 
+        // password should be expiring
+        dboConn = passwordExpiring( true, FIFTH_DB, DBO );
+        appleConn = passwordExpiring( true, FIFTH_DB, APPLE_USER );
+        
+        // setup so that passwords have expired after we reboot the database.
+        // shutdown the database so that the new property settings take effect.
+        goodStatement
+            ( dboConn, "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeMillis', '1' )" );
+        _fifthDBSetup.getTestConfiguration().shutdownDatabase();
+
+        // the DBO's password does not expire
+        dboConn = openConnection( FIFTH_DB, DBO );
+
+        // but the other user's password has expired
+        appleConn = getConnection( true, FIFTH_DB, APPLE_USER, INVALID_AUTHENTICATION );
+        
+        // setup so that passwords don't expire after we reboot the database.
+        // shutdown the database so that the new property settings take effect.
+        goodStatement
+            ( dboConn, "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeMillis', '0' )" );
+        _fifthDBSetup.getTestConfiguration().shutdownDatabase();
+
+        // passwords should NOT be expiring or expired
+        dboConn = passwordExpiring( false, FIFTH_DB, DBO );
+        appleConn = passwordExpiring( false, FIFTH_DB, APPLE_USER );
+
+        // check that invalid property settings are caught
+        expectExecutionError
+            (
+             dboConn, BAD_PASSWORD_PROPERTY,
+             "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeMillis', 'rabbit' )"
+             );
+        expectExecutionError
+            (
+             dboConn, BAD_PASSWORD_PROPERTY,
+             "call syscs_util.syscs_set_database_property( 'derby.authentication.native.passwordLifetimeThreshold', '-1' )"
+             );
+    }
+
     private void    vetSQLAuthorizationOn() throws Exception
     {
         Connection  nonDBOConn = openConnection( CREDENTIALS_DB, APPLE_USER );
@@ -515,6 +596,32 @@ public class NativeAuthenticationService
         return conn;
     }
 
+    // connect but expect a warning that the password is about to expire
+    private Connection  passwordExpiring( boolean expiring, String dbName, String user )
+        throws Exception
+    {
+        Connection  conn = null;
+
+        println( user + " attempting to get connection to database " + dbName );
+
+        conn = openConnection( dbName, user );
+
+        SQLWarning  warning = conn.getWarnings();
+
+        if ( expiring )
+        {
+            assertNotNull( tagError( "Should have seen a warning" ), warning );
+            assertSQLState( PASSWORD_EXPIRING, warning );
+        }
+        else
+        {
+            assertNull( tagError( "Should not have seen a warning" ), warning );
+        }
+
+
+        return conn;
+    }
+
     private void    addUser( Connection conn, String user ) throws Exception
     {
         String  password = getPassword( user );