/*

   Derby - Class org.apache.derby.impl.jdbc.authentication.NativeAuthenticationServiceImpl

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derby.impl.jdbc.authentication;

import java.util.Properties;
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;

import org.apache.derby.catalog.SystemProcedures;
import org.apache.derby.iapi.db.Database;
import org.apache.derby.iapi.reference.Property;
import org.apache.derby.iapi.services.info.JVMInfo;
import org.apache.derby.iapi.sql.dictionary.DataDictionary;
import org.apache.derby.iapi.sql.dictionary.PasswordHasher;
import org.apache.derby.iapi.sql.dictionary.UserDescriptor;
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;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.store.access.TransactionController;
import org.apache.derby.iapi.util.IdUtil;
import org.apache.derby.iapi.util.StringUtil;
import org.apache.derby.impl.jdbc.Util;
import org.apache.derby.jdbc.InternalDriver;

/**
 * 

* This authentication service supports Derby NATIVE authentication. *

* *

* To activate this service, set the derby.authentication.provider database * or system property to a value beginning with the token "NATIVE:". *

* *

* This service instantiates and calls the basic User authentication scheme at runtime. *

* *

* User credentials are defined in the SYSUSERS table. *

* */ public final class NativeAuthenticationServiceImpl extends AuthenticationServiceBase implements UserAuthenticator { /////////////////////////////////////////////////////////////////////////////////// // // CONSTANTS // /////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////// // // STATE // /////////////////////////////////////////////////////////////////////////////////// // temporary, used when bootstrapping a locally authenticated database private boolean _creatingCredentialsDB = false; 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; /////////////////////////////////////////////////////////////////////////////////// // // ModuleControl BEHAVIOR // /////////////////////////////////////////////////////////////////////////////////// /** * Check if we should activate this authentication service. */ public boolean canSupport(Properties properties) { if (!requireAuthentication(properties)) { return false; } if ( PropertyUtil.nativeAuthenticationEnabled( properties ) ) { parseNativeSpecification( properties ); return true; } else { return false; } } /** *

* Parse the specification of NATIVE authentication. It can take 3 forms: *

* * */ private void parseNativeSpecification( Properties properties ) { // If we get here, we already know that the authentication provider property // begins with the NATIVE: token String authenticationProvider = PropertyUtil.getPropertyFromSet ( properties, Property.AUTHENTICATION_PROVIDER_PARAMETER ); _authenticateDatabaseOperationsLocally = PropertyUtil.localNativeAuthenticationEnabled( properties ); // Everything between the first colon and the last colon is the name of a database int dbNameStartIdx = authenticationProvider.indexOf( ":" ) + 1; int dbNameEndIdx = _authenticateDatabaseOperationsLocally ? authenticationProvider.lastIndexOf( ":" ) : authenticationProvider.length(); if ( dbNameEndIdx > dbNameStartIdx ) { _credentialsDB = authenticationProvider.substring( dbNameStartIdx, dbNameEndIdx ); 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; } } } /** *

* Return true if AUTHENTICATION_PROVIDER_PARAMETER was well formatted. * The property must have designated some database as the authentication authority. *

*/ private boolean validAuthenticationProvider() throws StandardException { // If there is no store, then we are booting a system-wide authentication service boolean systemWideAuthentication = ( getServiceName() == null ); if ( _credentialsDB != null ) { // make sure that it is a well-formed database name if ( Monitor.getMonitor().getCanonicalServiceName( _credentialsDB ) == null ) { throw StandardException.newException( SQLState.BAD_CREDENTIALS_DB_NAME, _credentialsDB ); } else { return true; } } // must have a global credentials db for system-wide authentication if ( systemWideAuthentication ) { return false; } // so there is no credentials db specified and we are booting a database. // this is only allowed if we are authenticating locally in that database. return _authenticateDatabaseOperationsLocally; } /** * @see org.apache.derby.iapi.services.monitor.ModuleControl#boot * @exception StandardException upon failure to load/boot the expected * authentication service. */ public void boot(boolean create, Properties properties) throws StandardException { // first perform the initialization in our superclass super.boot( create, properties ); if ( !validAuthenticationProvider() ) { 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. try { MessageDigest digestAlgorithm = MessageDigest.getInstance("SHA-1"); digestAlgorithm.reset(); } catch (NoSuchAlgorithmException nsae) { throw Monitor.exceptionStartingModule(nsae); } // bootstrap the creation of the initial username/password when the dbo creates a credentials db if ( create && authenticatingInThisService( getCanonicalServiceName() ) ) { _creatingCredentialsDB = true; } else { _creatingCredentialsDB = false; } // Set ourselves as being ready, having loaded the proper // authentication scheme for this service // this.setAuthenticationService(this); } /////////////////////////////////////////////////////////////////////////////////// // // UserAuthenticator BEHAVIOR // /////////////////////////////////////////////////////////////////////////////////// /** Override behavior in superclass */ public String getSystemCredentialsDatabaseName() { return _credentialsDB; } /** Override behavior in superclass */ /** * Authenticate the passed-in user's credentials. * * @param userName The user's name used to connect to JBMS system * @param userPassword The user's password used to connect to JBMS system * @param databaseName The database which the user wants to connect to. * @param info Additional jdbc connection info. */ public boolean authenticateUser ( String userName, String userPassword, String databaseName, Properties info ) throws SQLException { try { // No "guest" user if ( userName == null ) { return false; } if ( userPassword == null ) { return false; } // // We must handle these cases: // // 1) Database name is null. This means that we are authenticating a system-wide // operation. The authentication must be done by the system-wide credentials database. // // 2) Database name is not null and authentication is NOT specified as local. // This means that we are authenticating a database-specific operation // in the system-wide credentials database. There are two subcases: // // 2a) The current database is NOT the credentials database. This reduces to case (1) above: // authentication must be performed in another database. // // 2b) The current database IS the credentials database. This reduces to case (3) below: // authentication must be performed in this database. // // 3) Database name is not null and authentication IS being performed locally in this database. // This means that we are authenticating a database-specific operation and performing the // authentication in this database. // if ( (databaseName == null) || !authenticatingInThisDatabase( databaseName ) ) { return authenticateRemotely( userName, userPassword, databaseName ); } else { return authenticateLocally( userName, userPassword, databaseName ); } } catch (StandardException se) { throw Util.generateCsSQLException(se); } } /** *

* Return true if we are authenticating in this database. *

*/ private boolean authenticatingInThisDatabase( String userVisibleDatabaseName ) throws StandardException { return authenticatingInThisService( Monitor.getMonitor().getCanonicalServiceName( userVisibleDatabaseName ) ); } /** *

* Return true if we are authenticating in this service. *

*/ private boolean authenticatingInThisService( String canonicalDatabaseName ) throws StandardException { if ( _authenticateDatabaseOperationsLocally ) { return true; } else { return isCredentialsService( canonicalDatabaseName ); } } /** *

* Return true if the passed in service is the credentials database. *

*/ private boolean isCredentialsService( String canonicalDatabaseName ) throws StandardException { String canonicalCredentialsDBName = getCanonicalServiceName( _credentialsDB ); String canonicalDB = Monitor.getMonitor().getCanonicalServiceName( canonicalDatabaseName ); if ( canonicalCredentialsDBName == null ) { return false; } else { return canonicalCredentialsDBName.equals( canonicalDatabaseName ); } } /** Get the canonical name of the current database service */ private String getCanonicalServiceName() throws StandardException { return getCanonicalServiceName( getServiceName() ); } /** Turn a service name into its normalized, standard form */ private String getCanonicalServiceName( String rawName ) throws StandardException { return Monitor.getMonitor().getCanonicalServiceName( rawName ); } /////////////////////////////////////////////////////////////////////////////////// // // AUTHENTICATE REMOTELY // /////////////////////////////////////////////////////////////////////////////////// /** * Authenticate the passed-in credentials against another Derby database. This is done * by getting a connection to the credentials database using the supplied username * and password. If the connection attempts succeeds, then authentication succeeds. * * @param userName The user's name used to connect to JBMS system * @param userPassword The user's password used to connect to JBMS system * @param databaseName The database which the user wants to connect to. */ private boolean authenticateRemotely ( String userName, String userPassword, String databaseName ) throws StandardException, SQLWarning { // this catches the case when someone specifies derby.authentication.provider=NATIVE::LOCAL // at the system level if ( _credentialsDB == null ) { throw StandardException.newException( SQLState.BAD_NATIVE_AUTH_SPEC ); } SQLWarning warnings = null; try { Properties properties = new Properties(); properties.setProperty( Attribute.USERNAME_ATTR, userName ); properties.setProperty( Attribute.PASSWORD_ATTR, userPassword ); String connectionURL = Attribute.PROTOCOL + _credentialsDB; Connection conn = InternalDriver.activeDriver().connect( connectionURL, properties, 0 ); warnings = conn.getWarnings(); conn.close(); } catch (SQLException se) { String sqlState = se.getSQLState(); if ( SQLState.LOGIN_FAILED.equals( sqlState ) ) { return false; } else if ( SQLState.DATABASE_NOT_FOUND.startsWith( sqlState ) ) { throw StandardException.newException( SQLState.MISSING_CREDENTIALS_DB, _credentialsDB ); } 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; } private StandardException wrap( Throwable t ) { return StandardException.plainWrapException( t ); } /////////////////////////////////////////////////////////////////////////////////// // // AUTHENTICATE LOCALLY // /////////////////////////////////////////////////////////////////////////////////// /** * Authenticate the passed-in credentials against the local database. * * @param userName The user's name used to connect to JBMS system * @param userPassword The user's password used to connect to JBMS system * @param databaseName The database which the user wants to connect to. */ private boolean authenticateLocally ( String userName, String userPassword, String databaseName ) throws StandardException, SQLException { userName = IdUtil.getUserAuthorizationId( userName ) ; // // Special bootstrap code. If we are creating a credentials database, then // we store the DBO's initial credentials in it. We also turn on NATIVE LOCAL authentication // forever. // if ( _creatingCredentialsDB ) { _creatingCredentialsDB = false; TransactionController tc = getTransaction(); SystemProcedures.addUser( userName, userPassword, tc ); tc.commit(); return true; } // // we expect to find a data dictionary // DataDictionary dd = (DataDictionary) Monitor.getServiceModule( this, DataDictionary.MODULE ); UserDescriptor userDescriptor = dd.getUser( userName ); if ( userDescriptor == null ) { // // Before returning, we pretend to evaluate the password. // This helps prevent blackhats from discovering legal usernames // by measuring how long password evaluation takes. For more context, // see the 2012-02-22 comment on DERBY-5539. // PasswordHasher hasher = dd.makePasswordHasher( getDatabaseProperties() ); hasher.hashPasswordIntoString( userName, userPassword ).toCharArray(); return false; } 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; } for ( int i = 0; i < candidatePassword.length; i++ ) { if ( candidatePassword[ i ] != actualPassword[ i ] ) { return false; } } } finally { if ( candidatePassword != null ) { Arrays.fill( candidatePassword, (char) 0 ); } if ( actualPassword != null ) { 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 ) { if ( dd.getAuthorizationDatabaseOwner().equals( userName ) ) { throw SQLWarningFactory.newSQLWarning( SQLState.DBO_PASSWORD_EXPIRES_SOON, databaseName ); } long daysRemaining = remainingLifetime / Property.MILLISECONDS_IN_DAY; throw SQLWarningFactory.newSQLWarning ( SQLState.PASSWORD_EXPIRES_SOON, Long.toString( daysRemaining ), databaseName ); } } return true; } }

Ads help maintain this website.