From feb6f6f0af2cbb3ebfb91f1bf546c23669b75e4a Mon Sep 17 00:00:00 2001
From: Jean-Noel Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Tue, 16 Apr 2013 13:57:43 +0000
Subject: [PATCH] OPENDJ-830 (CR-1554) Implement authentication and authorization for HTTP connection handler
---
opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java | 2
opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java | 110 +++++++++++++++++++++---------------
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java | 26 ++++++--
3 files changed, 84 insertions(+), 54 deletions(-)
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
index 1584322..7d99dab 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -48,6 +48,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ErrorResultException;
@@ -58,6 +59,7 @@
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
@@ -119,6 +121,9 @@
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
{
+ final boolean prettyPrint =
+ Boolean.parseBoolean(request.getParameter("_prettyPrint"));
+
final HTTPClientConnection clientConnection =
new HTTPClientConnection(this.connectionHandler, request);
this.connectionHandler.addClientConnection(clientConnection);
@@ -156,7 +161,9 @@
// The user could not be authenticated. Send an HTTP Basic authentication
// challenge if HTTP Basic authentication is enabled.
- sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+ sendErrorReponse(response, prettyPrint, ResourceException.getException(
+ HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
+
clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS, false,
null);
}
@@ -167,6 +174,8 @@
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
+ sendErrorReponse(response, prettyPrint, Rest2LDAP.asResourceException(e));
+
Message message =
INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientConnection
.getClientHostPort(), clientConnection.getServerHostPort(),
@@ -262,8 +271,11 @@
* the request where to extract the username and password from
* @return the array containing the username/password couple if both exist,
* null otherwise
+ * @throws ResourceException
+ * if any error occur
*/
String[] extractUsernamePassword(ServletRequest request)
+ throws ResourceException
{
HttpServletRequest req = (HttpServletRequest) request;
@@ -299,19 +311,25 @@
}
/**
- * Sends an Unauthorized status code and a challenge for HTTP Basic
- * authentication if HTTP basic authentication is enabled.
+ * Sends an error response back to the client. If the error response is
+ * "Unauthorized", then it will send a challenge for HTTP Basic authentication
+ * if HTTP Basic authentication is enabled.
*
* @param response
* where to send the Unauthorized status code.
+ * @param prettyPrint
+ * whether to format the JSON document output
+ * @param re
+ * the resource exception with the error response content
*/
- void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(
- ServletResponse response)
+ void sendErrorReponse(ServletResponse response, boolean prettyPrint,
+ ResourceException re)
{
HttpServletResponse resp = (HttpServletResponse) response;
- resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ resp.setStatus(re.getCode());
- if (authConfig.isBasicAuthenticationSupported())
+ if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED
+ && authConfig.isBasicAuthenticationSupported())
{
resp.setHeader("WWW-Authenticate",
"Basic realm=\"org.forgerock.opendj\"");
@@ -323,11 +341,7 @@
resp.setHeader("Content-Type", "application/json");
ServletOutputStream out = resp.getOutputStream();
- out.println("{");
- out.println(" \"code\": 401,");
- out.println(" \"message\": \"Invalid Credentials\",");
- out.println(" \"reason\": \"Unauthorized\"");
- out.println("}");
+ out.println(toJSON(prettyPrint, re));
}
catch (IOException ignore)
{
@@ -340,6 +354,32 @@
}
/**
+ * Returns a JSON representation of the {@link ResourceException}.
+ *
+ * @param prettyPrint
+ * whether to format the resulting JSON document
+ * @param re
+ * the resource exception to convert to a JSON document
+ * @return a String containing the JSON representation of the
+ * {@link ResourceException}.
+ */
+ private String toJSON(boolean prettyPrint, ResourceException re)
+ {
+ final String indent = "\n ";
+ final StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ if (prettyPrint) sb.append(indent);
+ sb.append("\"code\": ").append(re.getCode()).append(",");
+ if (prettyPrint) sb.append(indent);
+ sb.append("\"message\": \"").append(re.getMessage()).append("\",");
+ if (prettyPrint) sb.append(indent);
+ sb.append("\"reason\": \"").append(re.getReason()).append("\"");
+ if (prettyPrint) sb.append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
* Parses username and password from the authentication header used in HTTP
* basic authentication.
*
@@ -347,8 +387,10 @@
* the authentication header obtained from the request
* @return an array containing the username at index 0 and the password at
* index 1, or null if the header cannot be parsed successfully
+ * @throws ResourceException
+ * if the base64 password cannot be decoded
*/
- String[] parseUsernamePassword(String authHeader)
+ String[] parseUsernamePassword(String authHeader) throws ResourceException
{
if (authHeader != null
&& (authHeader.startsWith("Basic") || authHeader.startsWith("basic")))
@@ -370,10 +412,7 @@
}
catch (ParseException e)
{
- if (debugEnabled())
- {
- TRACER.debugCaught(DebugLogLevel.ERROR, e);
- }
+ throw Rest2LDAP.asResourceException(e);
}
}
return null;
@@ -392,9 +431,11 @@
* the connection to use for search and bind
* @return the {@link AuthenticationInfo} for the supplied credentials, null
* if authentication was unsuccessful.
+ * @throws ErrorResultException
+ * if a Rest2LDAP result comes back with an error or cancel state
*/
private AuthenticationInfo authenticate(String userName, String password,
- Connection connection)
+ Connection connection) throws ErrorResultException
{
// TODO JNR do the next steps in an async way
SearchResultEntry resultEntry = searchUniqueEntryDN(userName, connection);
@@ -411,7 +452,7 @@
}
private SearchResultEntry searchUniqueEntryDN(String userName,
- Connection connection)
+ Connection connection) throws ErrorResultException
{
// use configured rights to find the user DN
final Filter filter =
@@ -419,37 +460,16 @@
final SearchRequest searchRequest =
Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
.getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
- try
- {
- return connection.searchSingleEntry(searchRequest);
- }
- catch (ErrorResultException e)
- {
- if (debugEnabled())
- {
- TRACER.debugCaught(DebugLogLevel.ERROR, e);
- }
- }
- return null;
+ return connection.searchSingleEntry(searchRequest);
}
private boolean bind(String bindDN, String password, Connection connection)
+ throws ErrorResultException
{
- BindRequest bindRequest =
+ final BindRequest bindRequest =
Requests.newSimpleBindRequest(bindDN, password.getBytes());
- try
- {
- BindResult bindResult = connection.bind(bindRequest);
- return ResultCode.SUCCESS.equals(bindResult.getResultCode());
- }
- catch (ErrorResultException e)
- {
- if (debugEnabled())
- {
- TRACER.debugCaught(DebugLogLevel.ERROR, e);
- }
- }
- return false;
+ final BindResult bindResult = connection.bind(bindRequest);
+ return ResultCode.SUCCESS.equals(bindResult.getResultCode());
}
/** {@inheritDoc} */
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
index 361d045..8d82c76 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -119,7 +119,7 @@
* Indicates whether the Directory Server believes this connection to be valid
* and available for communication.
*/
- private volatile boolean connectionValid;
+ private volatile boolean connectionValid = true;
/**
* Indicates whether this connection is about to be closed. This will be used
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
index dfd09d6..5e18241 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -36,6 +36,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.forgerock.json.resource.ResourceException;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.util.Base64;
@@ -65,6 +66,7 @@
@Test(dataProvider = "Invalid HTTP basic auth strings")
public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader)
+ throws Exception
{
assertThat(filter.parseUsernamePassword(authZHeader)).isNull();
}
@@ -78,6 +80,7 @@
@Test(dataProvider = "Valid HTTP basic auth strings")
public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader)
+ throws Exception
{
assertThat(filter.parseUsernamePassword(authZHeader)).containsExactly(
USERNAME, PASSWORD);
@@ -92,7 +95,7 @@
ServletOutputStream oStream = mock(ServletOutputStream.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(response.getOutputStream()).thenReturn(oStream);
- filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+ sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
verify(response).setHeader("WWW-Authenticate",
@@ -109,25 +112,32 @@
HttpServletResponse response = mock(HttpServletResponse.class);
ServletOutputStream oStream = mock(ServletOutputStream.class);
when(response.getOutputStream()).thenReturn(oStream);
- filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+ sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
verifyUnauthorizedOutputMessage(response, oStream);
}
+ private void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(
+ HttpServletResponse response)
+ {
+ filter.sendErrorReponse(response, true, ResourceException.getException(
+ HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
+ }
+
private void verifyUnauthorizedOutputMessage(HttpServletResponse response,
ServletOutputStream oStream) throws IOException
{
verify(response).getOutputStream();
- verify(oStream).println("{");
- verify(oStream).println(" \"code\": 401,");
- verify(oStream).println(" \"message\": \"Invalid Credentials\",");
- verify(oStream).println(" \"reason\": \"Unauthorized\"");
- verify(oStream).println("}");
+ verify(oStream).println(
+ "{\n" + " \"code\": 401,\n"
+ + " \"message\": \"Invalid Credentials\",\n"
+ + " \"reason\": \"Unauthorized\"\n" + "}");
}
@Test
public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent()
+ throws Exception
{
authConfig.setBasicAuthenticationSupported(true);
@@ -140,7 +150,7 @@
}
@Test
- public void extractUsernamePasswordCustomHeaders()
+ public void extractUsernamePasswordCustomHeaders() throws Exception
{
final String customHeaderUsername = "X-OpenIDM-Username";
final String customHeaderPassword = "X-OpenIDM-Password";
--
Gitblit v1.10.0