From 85e5ab92ac335ce41ed775364459a9629b2c77c4 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 29 Sep 2011 09:50:04 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)
---
opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java | 230 +++++++++++++++++++++++++++++++-
opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java | 20 ++
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java | 116 +++++++++++-----
opendj-sdk/opends/resource/schema/02-config.ldif | 14 ++
4 files changed, 332 insertions(+), 48 deletions(-)
diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index 6458a2c..42625ff 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -2646,6 +2646,20 @@
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE
X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.26
+ NAME 'ds-pta-cached-password'
+ SYNTAX 1.3.6.1.4.1.26027.1.3.1
+ SINGLE-VALUE
+ NO-USER-MODIFICATION
+ USAGE directoryOperation
+ X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.27
+ NAME 'ds-pta-cached-password-time'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
+ SINGLE-VALUE
+ NO-USER-MODIFICATION
+ USAGE directoryOperation
+ X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
NAME 'ds-cfg-access-control-handler'
SUP top
diff --git a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java b/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
index 18cd876..232f815 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -23,6 +23,7 @@
*
*
* Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011 ForgeRock AS
*/
package org.opends.server.config;
@@ -3685,6 +3686,25 @@
/**
+ * The name of the operational attribute which will be put in user's entry in
+ * order to cache a copy of their password for pass through authentication.
+ */
+ public static final String OP_ATTR_PTAPOLICY_CACHED_PASSWORD =
+ "ds-pta-cached-password";
+
+
+
+ /**
+ * The name of the operational attribute which will be put in user's entry in
+ * order to record the time at which their password was cached for pass
+ * through authentication.
+ */
+ public static final String OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME =
+ "ds-pta-cached-password-time";
+
+
+
+ /**
* The name of the attribute option used to indicate that a configuration
* attribute has one or more pending values.
*/
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 8539712..500f1e1 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -30,6 +30,7 @@
import static org.opends.messages.ExtensionMessages.*;
+import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.protocols.ldap.LDAPConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
@@ -54,13 +55,18 @@
import org.opends.server.api.*;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
import org.opends.server.loggers.debug.DebugLogger;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.asn1.ASN1Exception;
+import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.*;
+import org.opends.server.schema.GeneralizedTimeSyntax;
+import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.*;
+import org.opends.server.util.TimeThread;
@@ -73,7 +79,6 @@
// TODO: handle password policy response controls? AD?
// TODO: custom aliveness pings
- // TODO: cache password
// TODO: improve debug logging and error messages.
/**
@@ -1527,12 +1532,23 @@
/**
- * Returns the current time in milli-seconds in order to perform cached
- * password expiration checks.
+ * Returns the current time in order to perform cached password expiration
+ * checks. The returned string will be formatted as a a generalized time
+ * string
*
- * @return The current time in milli-seconds.
+ * @return The current time.
*/
- long getCurrentTimeMillis();
+ String getCurrentTime();
+
+
+
+ /**
+ * Returns the current time in order to perform cached password expiration
+ * checks.
+ *
+ * @return The current time in MS.
+ */
+ long getCurrentTimeMS();
}
@@ -1616,13 +1632,22 @@
private final class StateImpl extends AuthenticationPolicyState
{
- private ByteString cachedPassword = null;
+ private final AttributeType cachedPasswordAttribute;
+ private final AttributeType cachedPasswordTimeAttribute;
+
+ private ByteString newCachedPassword = null;
+
private StateImpl(final Entry userEntry)
{
super(userEntry);
+
+ this.cachedPasswordAttribute = DirectoryServer.getAttributeType(
+ OP_ATTR_PTAPOLICY_CACHED_PASSWORD, true);
+ this.cachedPasswordTimeAttribute = DirectoryServer.getAttributeType(
+ OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME, true);
}
@@ -1633,10 +1658,52 @@
@Override
public void finalizeStateAfterBind() throws DirectoryException
{
- if (cachedPassword != null)
+ sharedLock.lock();
+ try
{
- // TODO: persist cached password if needed.
- cachedPassword = null;
+ if (cfg.isUsePasswordCaching() && newCachedPassword != null)
+ {
+ // Update the user's entry to contain the cached password and
+ // time stamp.
+ ByteString encodedPassword = pwdStorageScheme
+ .encodePasswordWithScheme(newCachedPassword);
+
+ List<RawModification> modifications =
+ new ArrayList<RawModification>(2);
+ modifications.add(RawModification.create(ModificationType.REPLACE,
+ OP_ATTR_PTAPOLICY_CACHED_PASSWORD, encodedPassword));
+ modifications.add(RawModification.create(ModificationType.REPLACE,
+ OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME,
+ provider.getCurrentTime()));
+
+ InternalClientConnection conn = InternalClientConnection
+ .getRootConnection();
+ ModifyOperation internalModify = conn.processModify(userEntry
+ .getDN().toString(), modifications);
+
+ ResultCode resultCode = internalModify.getResultCode();
+ if (resultCode != ResultCode.SUCCESS)
+ {
+ // The modification failed for some reason. This should not
+ // prevent the bind from succeeded since we are only updating
+ // cache data. However, the performance of the server may be
+ // impacted, so log a debug warning message.
+ if (debugEnabled())
+ {
+ TRACER.debugWarning(
+ "An error occurred while trying to update the LDAP PTA "
+ + "cached password for user %s: %s", userEntry.getDN()
+ .toString(), String.valueOf(internalModify
+ .getErrorMessage()));
+ }
+ }
+
+ newCachedPassword = null;
+ }
+ }
+ finally
+ {
+ sharedLock.unlock();
}
}
@@ -1663,8 +1730,13 @@
sharedLock.lock();
try
{
- // First of determine the user name to use when binding to the remote
- // directory.
+ // First check the cached password if enabled and available.
+ if (passwordMatchesCachedPassword(password))
+ {
+ return true;
+ }
+
+ // The cache lookup failed, so perform full PTA.
ByteString username = null;
switch (cfg.getMappingPolicy())
@@ -1820,6 +1892,11 @@
{
connection = bindFactory.getConnection();
connection.simpleBind(username, password);
+
+ // The password matched, so cache it, it will be stored in the
+ // user's entry when the state is finalized and only if caching is
+ // enabled.
+ newCachedPassword = password;
return true;
}
catch (final DirectoryException e)
@@ -1852,6 +1929,118 @@
sharedLock.unlock();
}
}
+
+
+
+ private boolean passwordMatchesCachedPassword(ByteString password)
+ {
+ if (!cfg.isUsePasswordCaching())
+ {
+ return false;
+ }
+
+ // First determine if the cached password time is present and valid.
+ boolean foundValidCachedPasswordTime = false;
+
+ List<Attribute> cptlist = userEntry
+ .getAttribute(cachedPasswordTimeAttribute);
+ if (cptlist != null && !cptlist.isEmpty())
+ {
+ foundCachedPasswordTime:
+ {
+ for (Attribute attribute : cptlist)
+ {
+ // Ignore any attributes with options.
+ if (!attribute.hasOptions())
+ {
+ for (AttributeValue value : attribute)
+ {
+ try
+ {
+ long cachedPasswordTime = GeneralizedTimeSyntax
+ .decodeGeneralizedTimeValue(value.getNormalizedValue());
+ long currentTime = provider.getCurrentTimeMS();
+ long expiryTime = cachedPasswordTime
+ + (cfg.getCachedPasswordTTL() * 1000);
+ foundValidCachedPasswordTime = (expiryTime > currentTime);
+ }
+ catch (DirectoryException e)
+ {
+ // Fall-through and give up immediately.
+ if (debugEnabled())
+ {
+ TRACER.debugCaught(DebugLogLevel.ERROR, e);
+ }
+ }
+
+ break foundCachedPasswordTime;
+ }
+ }
+ }
+ }
+ }
+
+ if (!foundValidCachedPasswordTime)
+ {
+ // The cached password time was not found or it has expired, so give
+ // up immediately.
+ return false;
+ }
+
+ // Next determine if there is a cached password.
+ ByteString cachedPassword = null;
+
+ List<Attribute> cplist = userEntry
+ .getAttribute(cachedPasswordAttribute);
+ if (cplist != null && !cplist.isEmpty())
+ {
+ foundCachedPassword:
+ {
+ for (Attribute attribute : cplist)
+ {
+ // Ignore any attributes with options.
+ if (!attribute.hasOptions())
+ {
+ for (AttributeValue value : attribute)
+ {
+ cachedPassword = value.getValue();
+ break foundCachedPassword;
+ }
+ }
+ }
+ }
+ }
+
+ if (cachedPassword == null)
+ {
+ // The cached password was not found, so give up immediately.
+ return false;
+ }
+
+ // Decode the password and match it according to its storage scheme.
+ try
+ {
+ String[] userPwComponents = UserPasswordSyntax
+ .decodeUserPassword(cachedPassword.toString());
+ PasswordStorageScheme<?> scheme = DirectoryServer
+ .getPasswordStorageScheme(userPwComponents[0]);
+ if (scheme != null)
+ {
+ return scheme.passwordMatches(password,
+ ByteString.valueOf(userPwComponents[1]));
+ }
+ }
+ catch (DirectoryException e)
+ {
+ // Unable to decode the cached password, so give up.
+ if (debugEnabled())
+ {
+ TRACER.debugCaught(DebugLogLevel.ERROR, e);
+ }
+ }
+
+ return false;
+ }
}
@@ -1867,6 +2056,8 @@
private ConnectionFactory searchFactory = null;
private ConnectionFactory bindFactory = null;
+ private PasswordStorageScheme<?> pwdStorageScheme = null;
+
private PolicyImpl(
@@ -2062,6 +2253,12 @@
bindFactory = new FailoverLoadBalancer(primaryBindLoadBalancer,
secondaryBindLoadBalancer, scheduler);
}
+
+ if (cfg.isUsePasswordCaching())
+ {
+ pwdStorageScheme = DirectoryServer.getPasswordStorageScheme(cfg
+ .getCachedPasswordStorageSchemeDN());
+ }
}
@@ -2137,9 +2334,16 @@
- public long getCurrentTimeMillis()
+ public String getCurrentTime()
{
- return System.currentTimeMillis();
+ return TimeThread.getGMTTime();
+ }
+
+
+
+ public long getCurrentTimeMS()
+ {
+ return TimeThread.getTime();
}
};
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index 6689eca..801995d 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -56,10 +56,12 @@
import org.opends.server.protocols.ldap.*;
import org.opends.server.schema.DirectoryStringSyntax;
import org.opends.server.schema.GeneralizedTimeSyntax;
+import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.*;
import org.opends.server.util.StaticUtils;
+import org.opends.server.util.TimeThread;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -877,7 +879,7 @@
private final Queue<Event<?>> expectedEvents = new LinkedList<Event<?>>();
private final List<MockScheduledFuture> monitorRunnables = new LinkedList<MockScheduledFuture>();
- private long currentTimeMS = System.currentTimeMillis();
+ private String currentTime = TimeThread.getGMTTime();
// All methods unused excepted scheduleWithFixedDelay.
private final ScheduledExecutorService mockScheduler = new ScheduledExecutorService()
@@ -1062,9 +1064,27 @@
/**
* {@inheritDoc}
*/
- public long getCurrentTimeMillis()
+ public String getCurrentTime()
{
- return currentTimeMS;
+ return currentTime;
+ }
+
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public long getCurrentTimeMS()
+ {
+ try
+ {
+ return GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ByteString
+ .valueOf(currentTime));
+ }
+ catch (DirectoryException e)
+ {
+ throw new RuntimeException(e);
+ }
}
@@ -1101,9 +1121,9 @@
- MockProvider withCurrentTime(final long currentTimeMS)
+ MockProvider withCurrentTime(final String currentTime)
{
- this.currentTimeMS = currentTimeMS;
+ this.currentTime = currentTime;
return this;
}
@@ -4271,7 +4291,7 @@
* @throws Exception
* If an unexpected exception occurred.
*/
- @Test(enabled = false, dataProvider = "testPasswordCachingData")
+ @Test(enabled = true, dataProvider = "testPasswordCachingData")
public void testPasswordCaching(String cacheState, boolean matchesCache,
boolean matchesReal) throws Exception
{
@@ -4279,20 +4299,23 @@
TestCaseUtils.initializeTestBackend(true);
// Choose arbitrary date.
- final GregorianCalendar testTime = new GregorianCalendar(2010, 0, 1);
- final long testTimeMS = testTime.getTimeInMillis();
+ final String testCurrentTimeGMT = "20100621120000Z";
- final String testCachedPassword = "{SSHA}N8QSu9kXHkODFxFtwtwVEqM5XMCfnSaq/5gWew==";
final boolean expectPTA;
+ final boolean expectCacheInfo;
final boolean expectCacheUpdate;
final boolean expectedBindResultIsSuccess;
final Entry testUser;
+ final String testCachedPassword = "{SSHA}N8QSu9kXHkODFxFtwtwVEqM5XMCfnSaq/5gWew==";
+ final String testCachedPasswordTime;
if (cacheState.equals("notPresent"))
{
expectPTA = true;
+ expectCacheInfo = matchesReal;
expectCacheUpdate = matchesReal;
expectedBindResultIsSuccess = matchesReal;
+ testCachedPasswordTime = null;
testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
@@ -4306,9 +4329,11 @@
}
else if (cacheState.equals("presentValid"))
{
- expectPTA = false;
+ expectPTA = !matchesCache;
+ expectCacheInfo = true;
expectCacheUpdate = !matchesCache && matchesReal;
- expectedBindResultIsSuccess = matchesCache;
+ expectedBindResultIsSuccess = matchesCache | matchesReal;
+ testCachedPasswordTime = "20100621110000Z"; // 1 hour old
// Create an entry whose cached password is 10s old.
testUser = TestCaseUtils.makeEntry(
@@ -4319,7 +4344,7 @@
"sn: user",
"cn: test user",
"ds-pta-cached-password: " + testCachedPassword,
- "ds-pta-cached-password-time: " + GeneralizedTimeSyntax.format(testTimeMS - 10000)
+ "ds-pta-cached-password-time: " + testCachedPasswordTime
/* @formatter:on */
);
}
@@ -4327,8 +4352,10 @@
{
// presentExpired
expectPTA = true;
+ expectCacheInfo = true;
expectCacheUpdate = matchesReal;
expectedBindResultIsSuccess = matchesReal;
+ testCachedPasswordTime = "20100620110000Z"; // 1 day + 1 hour old
// Create an entry whose cached password is more than 1 day old.
testUser = TestCaseUtils.makeEntry(
@@ -4339,7 +4366,7 @@
"sn: user",
"cn: test user",
"ds-pta-cached-password: " + testCachedPassword,
- "ds-pta-cached-password-time: " + GeneralizedTimeSyntax.format(testTimeMS - 100000)
+ "ds-pta-cached-password-time: " + testCachedPasswordTime
/* @formatter:on */
);
}
@@ -4359,24 +4386,25 @@
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
- .withPrimaryServer(phost1).withMappedAttribute("uid")
- .withBaseDN("o=ad").withUsePasswordCaching(true);
+ .withPrimaryServer(phost1).withUsePasswordCaching(true);
// Create all the events.
- final MockProvider provider = new MockProvider();
+ final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
+ phost1, cfg);
+
+ final MockProvider provider = new MockProvider().withCurrentTime(
+ testCurrentTimeGMT).expectEvent(fe);
if (expectPTA)
{
- final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
- phost1, cfg);
- provider.expectEvent(fe);
-
// Get connection then bind twice creating two pooled connections.
final GetConnectionEvent ce = new GetConnectionEvent(fe);
provider
.expectEvent(ce)
.expectEvent(
- new SimpleBindEvent(ce, "cn=test user,o=test", presentedPassword))
+ new SimpleBindEvent(ce, "cn=test user,o=test", presentedPassword,
+ expectedBindResultIsSuccess ? ResultCode.SUCCESS
+ : ResultCode.INVALID_CREDENTIALS))
.expectEvent(new CloseEvent(ce));
}
@@ -4388,28 +4416,46 @@
// Perform the authentication.
final AuthenticationPolicyState state = policy
- .createAuthenticationPolicyState(userEntry);
+ .createAuthenticationPolicyState(testUser);
assertEquals(state.getAuthenticationPolicy(), policy);
assertEquals(state.passwordMatches(ByteString.valueOf(presentedPassword)),
expectedBindResultIsSuccess);
state.finalizeStateAfterBind();
// Check that the password has been cached if needed.
- if (expectCacheUpdate)
+ Entry updatedTestUser = DirectoryServer.getEntry(DN
+ .decode("cn=test user,o=test"));
+
+ String newCachedPassword = updatedTestUser.getAttributeValue(
+ DirectoryServer.getAttributeType("ds-pta-cached-password"),
+ DirectoryStringSyntax.DECODER);
+
+ String newCachedPasswordTime = updatedTestUser.getAttributeValue(
+ DirectoryServer.getAttributeType("ds-pta-cached-password-time"),
+ DirectoryStringSyntax.DECODER);
+
+ if (expectCacheInfo)
{
- Entry updatedTestUser = DirectoryServer.getEntry(DN
- .decode("cn=test user,o=test"));
+ assertNotNull(newCachedPassword);
+ assertNotNull(newCachedPasswordTime);
- String newCachedPassword = updatedTestUser.getAttributeValue(
- DirectoryServer.getAttributeType("ds-pta-cached-password"),
- DirectoryStringSyntax.DECODER);
- assertFalse(newCachedPassword.equals(testCachedPassword));
-
- String newCachedPasswordTime = updatedTestUser.getAttributeValue(
- DirectoryServer.getAttributeType("ds-pta-cached-password-time"),
- DirectoryStringSyntax.DECODER);
- assertEquals(newCachedPasswordTime,
- GeneralizedTimeSyntax.format(testTimeMS));
+ if (expectCacheUpdate)
+ {
+ assertFalse(newCachedPassword.equals(testCachedPassword));
+ assertTrue(UserPasswordSyntax.isEncoded(ByteString
+ .valueOf(newCachedPassword)));
+ assertEquals(newCachedPasswordTime, testCurrentTimeGMT);
+ }
+ else
+ {
+ assertEquals(newCachedPassword, testCachedPassword);
+ assertEquals(newCachedPasswordTime, testCachedPasswordTime);
+ }
+ }
+ else
+ {
+ assertNull(newCachedPassword);
+ assertNull(newCachedPasswordTime);
}
// Tear down and check final state.
--
Gitblit v1.10.0