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

Jean-Noel Rouvignac
23.13.2013 202b9cdeebbd40337f0de6ae106d6b5293d1ea46
OPENDJ-830 (CR-1591) Implement authentication and authorization for HTTP connection handler

Implemented the authentication steps (search username + bind) in an async way.

CollectClientConnectionsFilter.java:
Added inner classes HTTPRequestContext, DoBindResultHandler and CallDoFilterResultHandler to support async processing.
In doFilter(), built the HTTPRequestContext + removed some local parameters.
Extracted the methods doFilter(HTTPRequestContext, AuthenticationInfo), sendAuthenticationFailure() and onFailure() from doFilter(ServletRequest, ServletResponse, FilterChain).
In several methods, replaced several parameters by using just one HTTPRequestContext parameter.
Extracted method buildSearchRequest(String) from searchUniqueEntryDN().
Removed getAuthenticationInfo() and added the code back to doFilter().
Removed authenticate(), searchUniqueEntryDN() and bind(), and moved their corresponding code to the inner classes.
Added getAsyncContext().

CollectClientConnectionsFilter.java:
Removed the test for authenticate()

SdkConnectionAdapter.java:
Fixed two bugs
3 files modified
316 ■■■■■ changed files
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java 292 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java 4 ●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java 20 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -36,9 +36,11 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Collection;
import javax.servlet.AsyncContext;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
@@ -53,7 +55,7 @@
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.ResultHandler;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -78,6 +80,105 @@
final class CollectClientConnectionsFilter implements javax.servlet.Filter
{
  /** This class holds all the necessary data to complete an HTTP request. */
  private final class HTTPRequestContext
  {
    private AsyncContext asyncContext;
    private ServletRequest request;
    private ServletResponse response;
    private FilterChain chain;
    private HTTPClientConnection clientConnection;
    private Connection connection;
    /** Whether to pretty print the resulting JSON. */
    private boolean prettyPrint;
    /** Can be used for the bind request. */
    private String password;
  }
  /**
   * This result handler calls {@link javax.servlet.Filter#doFilter()} after a
   * successful bind.
   */
  private final class CallDoFilterResultHandler implements
      ResultHandler<BindResult>
  {
    private final HTTPRequestContext ctx;
    private final SearchResultEntry resultEntry;
    private CallDoFilterResultHandler(HTTPRequestContext ctx,
        SearchResultEntry resultEntry)
    {
      this.ctx = ctx;
      this.resultEntry = resultEntry;
    }
    @Override
    public void handleErrorResult(ErrorResultException error)
    {
      onFailure(error, ctx);
    }
    @Override
    public void handleResult(BindResult result)
    {
      final AuthenticationInfo authInfo =
          new AuthenticationInfo(to(resultEntry), to(resultEntry.getName()),
              ByteString.valueOf(ctx.password), false);
      try
      {
        doFilter(ctx, authInfo);
      }
      catch (Exception e)
      {
        onFailure(e, ctx);
      }
    }
  }
  /**
   * This result handler invokes a bind after a successful search on the user
   * name used for authentication.
   */
  private final class DoBindResultHandler implements
      ResultHandler<SearchResultEntry>
  {
    private HTTPRequestContext ctx;
    private DoBindResultHandler(HTTPRequestContext ctx)
    {
      this.ctx = ctx;
    }
    @Override
    public void handleErrorResult(ErrorResultException error)
    {
      onFailure(error, ctx);
    }
    @Override
    public void handleResult(SearchResultEntry resultEntry)
    {
      final DN bindDN = resultEntry.getName();
      if (bindDN == null)
      {
        sendAuthenticationFailure(ctx);
      }
      else
      {
        final BindRequest bindRequest =
            Requests.newSimpleBindRequest(bindDN.toString(), ctx.password
                .getBytes(Charset.forName("UTF-8")));
        ctx.connection.bindAsync(bindRequest, null,
            new CallDoFilterResultHandler(ctx, resultEntry));
      }
    }
  }
  /** HTTP Header sent by the client with HTTP basic authentication. */
  static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
@@ -127,6 +228,15 @@
    final HTTPClientConnection clientConnection =
        new HTTPClientConnection(this.connectionHandler, request);
    this.connectionHandler.addClientConnection(clientConnection);
    final HTTPRequestContext ctx = new HTTPRequestContext();
    ctx.request = request;
    ctx.response = response;
    ctx.chain = chain;
    ctx.clientConnection = clientConnection;
    ctx.prettyPrint = prettyPrint;
    try
    {
      if (!canProcessRequest(request, clientConnection))
@@ -137,53 +247,95 @@
      // checked.
      logConnect(clientConnection);
      Connection connection = new SdkConnectionAdapter(clientConnection);
      ctx.connection = new SdkConnectionAdapter(clientConnection);
      AuthenticationInfo authInfo = getAuthenticationInfo(request, connection);
      if (authInfo != null)
      final String[] userPassword = extractUsernamePassword(request);
      if (userPassword != null && userPassword.length == 2)
      {
        clientConnection.setAuthenticationInfo(authInfo);
        final String userName = userPassword[0];
        ctx.password = userPassword[1];
        /*
         * WARNING: This action triggers 3-4 others: Set the connection for use
         * with this request on the HttpServletRequest. It will make
         * Rest2LDAPContextFactory create an AuthenticatedConnectionContext
         * which will in turn ensure Rest2LDAP uses the supplied Connection
         * object.
         */
        request.setAttribute(
            Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, connection);
        final AsyncContext asyncContext = getAsyncContext(request);
        asyncContext.setTimeout(60 * 1000);
        ctx.asyncContext = asyncContext;
        // send the request further down the filter chain or pass to servlet
        chain.doFilter(request, response);
        return;
        ctx.connection.searchSingleEntryAsync(buildSearchRequest(userName),
            new DoBindResultHandler(ctx));
      }
      // The user could not be authenticated. Send an HTTP Basic authentication
      // challenge if HTTP Basic authentication is enabled.
      sendErrorReponse(response, prettyPrint, ResourceException.getException(
          HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
      clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS, false,
          null);
      else if (this.connectionHandler.acceptUnauthenticatedRequests())
      {
        // use unauthenticated user
        doFilter(ctx, new AuthenticationInfo());
      }
      else
      {
        sendAuthenticationFailure(ctx);
      }
    }
    catch (Exception e)
    {
      onFailure(e, ctx);
    }
  }
  private void doFilter(HTTPRequestContext ctx, AuthenticationInfo authInfo)
      throws Exception
  {
    ctx.clientConnection.setAuthenticationInfo(authInfo);
    /*
     * WARNING: This action triggers 3-4 others: Set the connection for use with
     * this request on the HttpServletRequest. It will make
     * Rest2LDAPContextFactory create an AuthenticatedConnectionContext which
     * will in turn ensure Rest2LDAP uses the supplied Connection object.
     */
    ctx.request.setAttribute(
        Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, ctx.connection);
    // send the request further down the filter chain or pass to servlet
    ctx.chain.doFilter(ctx.request, ctx.response);
  }
  private void sendAuthenticationFailure(HTTPRequestContext ctx)
  {
    // The user could not be authenticated. Send an HTTP Basic authentication
    // challenge if HTTP Basic authentication is enabled.
    ResourceException unauthorizedException =
        ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED,
            "Invalid Credentials");
    sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException);
    ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS,
        false, null);
  }
  private void onFailure(Exception e, HTTPRequestContext ctx)
  {
    try
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      sendErrorReponse(response, prettyPrint, Rest2LDAP.asResourceException(e));
      sendErrorReponse(ctx.response, ctx.prettyPrint, Rest2LDAP
          .asResourceException(e));
      Message message =
          INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientConnection
              .getClientHostPort(), clientConnection.getServerHostPort(),
          INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection
              .getClientHostPort(), ctx.clientConnection.getServerHostPort(),
              getExceptionMessage(e));
      logError(message);
      clientConnection
          .disconnect(DisconnectReason.SERVER_ERROR, false, message);
      ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
          message);
    }
    finally
    {
      if (ctx.asyncContext != null)
      {
        ctx.asyncContext.complete();
      }
    }
  }
@@ -230,37 +382,6 @@
  }
  /**
   * Returns an {@link AuthenticationInfo} object if the request is accepted. An
   * {@link AuthenticationInfo} object will be returned if authentication
   * credentials were valid or if unauthenticated requests are allowed on this
   * server.
   *
   * @param request
   *          the request used to extract the {@link AuthenticationInfo}
   * @param connection
   *          the connection used to retrieve the {@link AuthenticationInfo}
   * @return an {@link AuthenticationInfo} if the request is accepted, null if
   *         the request was rejected
   * @throws Exception
   *           if any problem occur
   */
  AuthenticationInfo getAuthenticationInfo(ServletRequest request,
      Connection connection) throws Exception
  {
    String[] userPassword = extractUsernamePassword(request);
    if (userPassword != null && userPassword.length == 2)
    {
      return authenticate(userPassword[0], userPassword[1], connection);
    }
    else if (this.connectionHandler.acceptUnauthenticatedRequests())
    {
      // return unauthenticated
      return new AuthenticationInfo();
    }
    return null;
  }
  /**
   * Extracts the username and password from the request using one of the
   * enabled authentication mechanism: HTTP Basic authentication or HTTP Custom
   * headers. If no username and password can be obtained, then send back an
@@ -418,58 +539,19 @@
    return null;
  }
  /**
   * Authenticates the user by doing a search on the user name + bind the
   * returned user entry DN, then return the authentication info if search and
   * bind were successful.
   *
   * @param userName
   *          the user name to authenticate
   * @param password
   *          the password to use with the user
   * @param connection
   *          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) throws ErrorResultException
  private AsyncContext getAsyncContext(ServletRequest request)
  {
    // TODO JNR do the next steps in an async way
    SearchResultEntry resultEntry = searchUniqueEntryDN(userName, connection);
    if (resultEntry != null)
    {
      DN bindDN = resultEntry.getName();
      if (bindDN != null && bind(bindDN.toString(), password, connection))
      {
        return new AuthenticationInfo(to(resultEntry), to(bindDN), ByteString
            .valueOf(password), false);
      }
    }
    return null;
    return request.isAsyncStarted() ? request.getAsyncContext() : request
        .startAsync();
  }
  private SearchResultEntry searchUniqueEntryDN(String userName,
      Connection connection) throws ErrorResultException
  private SearchRequest buildSearchRequest(String userName)
  {
    // use configured rights to find the user DN
    final Filter filter =
        Filter.format(authConfig.getSearchFilterTemplate(), userName);
    final SearchRequest searchRequest =
        Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
    return Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
            .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
    return connection.searchSingleEntry(searchRequest);
  }
  private boolean bind(String bindDN, String password, Connection connection)
      throws ErrorResultException
  {
    final BindRequest bindRequest =
        Requests.newSimpleBindRequest(bindDN, password.getBytes());
    final BindResult bindResult = connection.bind(bindRequest);
    return ResultCode.SUCCESS.equals(bindResult.getResultCode());
  }
  /** {@inheritDoc} */
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
@@ -193,7 +193,7 @@
      IntermediateResponseHandler intermediateResponseHandler,
      ResultHandler<? super BindResult> resultHandler)
  {
    final int messageID = nextMessageID.get();
    final int messageID = nextMessageID.getAndIncrement();
    String userName = request.getName();
    byte[] password = ((SimpleBindRequest) request).getPassword();
    BindOperationBasis operation =
@@ -211,7 +211,7 @@
    AuthenticationInfo authInfo = this.clientConnection.getAuthenticationInfo();
    if (authInfo != null && authInfo.isAuthenticated())
    {
      final int messageID = nextMessageID.get();
      final int messageID = nextMessageID.getAndIncrement();
      UnbindOperationBasis operation =
          new UnbindOperationBasis(clientConnection, messageID, messageID,
              to(request.getControls()));
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -38,7 +38,6 @@
import org.forgerock.json.resource.ResourceException;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.util.Base64;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -167,23 +166,4 @@
        USERNAME, PASSWORD);
  }
  /**
   * Tests that getAuthenticationInfo() without basic auth header or custom
   * headers returns an unauthenticated info when the server accepts
   * unauthenticated requests.
   */
  @Test
  public void getAuthenticationInfoReturnsUnauthenticatedInfo()
      throws Exception
  {
    HttpServletRequest request = mock(HttpServletRequest.class);
    HTTPConnectionHandler cfg = mock(HTTPConnectionHandler.class);
    when(cfg.acceptUnauthenticatedRequests()).thenReturn(true);
    filter = new CollectClientConnectionsFilter(cfg, authConfig);
    AuthenticationInfo authInfo = filter.getAuthenticationInfo(request, null);
    assertThat(authInfo).isNotNull();
    assertThat(authInfo.isAuthenticated()).isFalse();
  }
}