mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Jean-Noel Rouvignac
16.57.2013 ad85152c76d37c2a455debd815e239d2ca812c78
OPENDJ-830 (CR-1554) Implement authentication and authorization for HTTP connection handler


Improved error reporting to the client: Internal exceptions are now returned to the client as such.
This simplified the error handling code.


CollectClientConnectionsFilter.java:
Renamed sendUnauthorizedResponseWithHTTPBasicAuthChallenge() to sendErrorResponse().
Extracted more generic method toJSON() from sendErrorResponse().
Removed several try / catch + logging and replaced them with throwing the exception up.

HTTPClientConnection.java:
A connection is now valid by default (doh!)

CollectClientConnectionsFilterTest.java:
Updated the tests.
3 files modified
134 ■■■■■ changed files
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java 106 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java 2 ●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java 26 ●●●●● patch | view | raw | blame | history
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,38 +460,17 @@
    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;
  }
  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);
    final BindResult bindResult = connection.bind(bindRequest);
      return ResultCode.SUCCESS.equals(bindResult.getResultCode());
    }
    catch (ErrorResultException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    }
    return false;
  }
  /** {@inheritDoc} */
  @Override
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
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";