UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleSASLMechanismHandler.java

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * docs/licenses/cddl.txt.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2010-2023 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.directory.sdk.common.operation.SASLBindRequest;
import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.ds.api.SASLMechanismHandler;
import com.unboundid.directory.sdk.ds.config.SASLMechanismHandlerConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.directory.sdk.ds.types.SASLBindResult;
import com.unboundid.directory.sdk.ds.types.SASLBindResultFactory;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentParser;



/**
 * This class provides a simple implementation of a SASL mechanism handler
 * that authenticates a user with a simple username and password (much like SASL
 * PLAIN, but without the need to specify the username as an authorization ID,
 * and without support for an alternate authorization identity).  The SASL
 * credentials should simply be a string in the form "username:password".  It
 * does not take any arguments.
 */
public final class ExampleSASLMechanismHandler
       extends SASLMechanismHandler
{
  // The server context for this extension.
  private DirectoryServerContext serverContext = null;



  /**
   * Creates a new instance of this SASL mechanism handler.  All SASL mechanism
   * handler implementations must include a default constructor, but any
   * initialization should generally be performed in the
   * {@code initializeSASLMechanismHandler} method.
   */
  public ExampleSASLMechanismHandler()
  {
    // No implementation required.
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example SASL Mechanism Handler";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This SASL mechanism handler serves as an example that may be used " +
           "to demonstrate the process for creating a third-party SASL " +
           "mechanism handler.  It uses a SASL mechanism name of " +
           "'EXAMPLE-USERNAME-AND-PASSWORD' and requires SASL credentials " +
           "to be in the form 'username:password'.  The SASL mechanism " +
           "handler must be configured with an identity mapper, which will " +
           "be used to map the username to the corresponding user entry."
    };
  }



  /**
   * Initializes this SASL mechanism handler.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this SASL mechanism
   *                        handler.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this SASL mechanism handler.
   *
   * @throws  LDAPException  If a problem occurs while initializing this SASL
   *                         mechanism handler.
   */
  @Override()
  public void initializeSASLMechanismHandler(
                   final DirectoryServerContext serverContext,
                   final SASLMechanismHandlerConfig config,
                   final ArgumentParser parser)
         throws LDAPException
  {
    this.serverContext = serverContext;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isConfigurationAcceptable(
                      final SASLMechanismHandlerConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    // Make sure that an identity mapper is included in the configuration.
    if (config.getIdentityMapperConfigEntryDN() == null)
    {
      unacceptableReasons.add("The EXAMPLE-USERNAME-AND-PASSWORD SASL " +
           "mechanism handler must be configured with an identity mapper.");
      return false;
    }

    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public ResultCode applyConfiguration(final SASLMechanismHandlerConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    return ResultCode.SUCCESS;
  }



  /**
   * Retrieves a list of the names of the SASL mechanisms supported by this
   * SASL mechanism handler.  This method will be invoked only immediately after
   * the {@link #initializeSASLMechanismHandler} method is called.
   *
   * @return  A list of the names of the SASL mechanisms supported by this SASL
   *          mechanism handler.
   */
  @Override()
  public List<String> getSASLMechanismNames()
  {
    // This handler only implements support for one SASL mechanism.
    return Arrays.asList("EXAMPLE-USERNAME-AND-PASSWORD");
  }



  /**
   * Indicates whether the SASL authentication process using the specified
   * mechanism may be considered secure (i.e., that a third party able to
   * observe the communication, potentially over an insecure communication
   * channel, would not be able to reproduce the authentication process).
   *
   * @param  mechanism  The name of the mechanism for which to make the
   *                    determination.  This will only be invoked with names of
   *                    mechanisms returned by the
   *                    {@link #getSASLMechanismNames} method.
   *
   * @return  {@code true} if the specified SASL mechanism should be considered
   *          secure, or {@code false} if not.
   */
  @Override()
  public boolean isSecure(final String mechanism)
  {
    // This SASL mechanism isn't inherently secure, since the credentials aren't
    // obscured in any way.  It would only be secure if used over an encrypted
    // connection (e.g., using SSL or StartTLS).
    return false;
  }



  /**
   * Indicates whether the SASL authentication process using the specified
   * mechanism involves the use of a password stored locally in the server
   * (optionally in combination with other forms of credentials).
   *
   * @param  mechanism  The name of the mechanism for which to make the
   *                    determination.  This will only be invoked with names of
   *                    mechanisms returned by the
   *                    {@link #getSASLMechanismNames} method.
   *
   * @return  {@code true} if the specified SASL mechanism makes use of a local
   *          password, or {@code false} if not.
   */
  @Override()
  public boolean isPasswordBased(final String mechanism)
  {
    // This SASL mechanism is password based.
    return true;
  }



  /**
   * Performs the appropriate processing for the provided SASL bind request.
   *
   * @param  operationContext  The context for the bind operation.
   * @param  bindRequest       The SASL bind request to be processed.
   * @param  resultFactory     A factory object that will be used to construct
   *                           the result to return.
   *
   * @return  An object with information about the result of the SASL bind
   *          processing.
   */
  @Override()
  public SASLBindResult processSASLBind(final OperationContext operationContext,
                             final SASLBindRequest bindRequest,
                             final SASLBindResultFactory resultFactory)
  {
    // The bind request must include SASL credentials.
    final ASN1OctetString saslCredentials = bindRequest.getSASLCredentials();
    if (saslCredentials == null)
    {
      return resultFactory.createFailureResult(
           "No SASL credentials included in the bind request.",
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           null);  // No unsuccessfully authenticated user DN is available.
    }


    // Extract the  username and password elements from the SASL credentials
    final String credString = saslCredentials.stringValue();
    final int colonPos = credString.indexOf(':');
    if (colonPos < 0)
    {
      return resultFactory.createFailureResult(
           "Malformed SASL credentials:  no colon to separate the username " +
                "from the password.",
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           null);  // No unsuccessfully authenticated user DN is available.
    }

    final String username = credString.substring(0, colonPos);
    if (username.length() == 0)
    {
      return resultFactory.createFailureResult(
           "Malformed SASL credentials:  Empty username.",
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           null);  // No unsuccessfully authenticated user DN is available.
    }

    final String password = credString.substring(colonPos+1);
    if (password.length() == 0)
    {
      return resultFactory.createFailureResult(
           "Malformed SASL credentials:  Empty password.",
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           null);  // No unsuccessfully authenticated user DN is available.
    }


    // Get the entry that corresponds to the provided username.
    final Entry userEntry;
    try
    {
      userEntry = resultFactory.mapUsernameToEntry(username);
      if (userEntry == null)
      {
        return resultFactory.createFailureResult(
             "Could not find entry for username '" + username + "'.",
             null,   // No message should be returned to the client.
             null,   // No matched DN is needed.
             null,   // No controls are needed.
             null,   // No server SASL credentials are required.
             null);  // No unsuccessfully authenticated user DN is available.
      }
      else
      {
        serverContext.debugInfo("Successfully mapped username '" + username +
             "' to entry '" + userEntry.getDN() + "'.");
      }
    }
    catch (final LDAPException le)
    {
      serverContext.debugCaught(le);
      return resultFactory.createFailureResult(
           "Could not find entry for username '" + username + "':  " +
                StaticUtils.getExceptionMessage(le),
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           null);  // No unsuccessfully authenticated user DN is available.
    }


    // Make sure that the password is valid for the user.
    try
    {
      final ASN1OctetString pw = new ASN1OctetString(password);
      if (! resultFactory.isUserPasswordValid(userEntry.getDN(), pw))
      {
        return resultFactory.createFailureResult(
             "The supplied password was not valid for user '" +
                  userEntry.getDN() + "'.",
             null,   // No message should be returned to the client.
             null,   // No matched DN is needed.
             null,   // No controls are needed.
             null,   // No server SASL credentials are required.
             userEntry.getDN());
      }
    }
    catch (final LDAPException le)
    {
      serverContext.debugCaught(le);
      return resultFactory.createFailureResult(
           "Unable to verify password for user '" + userEntry.getDN() + "':  " +
                StaticUtils.getExceptionMessage(le),
           null,   // No message should be returned to the client.
           null,   // No matched DN is needed.
           null,   // No controls are needed.
           null,   // No server SASL credentials are required.
           userEntry.getDN());
    }


    // If we've gotten here, then the authentication is considered successful.
    return resultFactory.createSuccessResult(userEntry.getDN(),
         userEntry.getDN(), null, null, null, new ASN1OctetString(password));
  }



  /**
   * Retrieves a map containing examples of configurations that may be used for
   * this extension.  The map key should be a list of sample arguments, and the
   * corresponding value should be a description of the behavior that will be
   * exhibited by the extension when used with that configuration.
   *
   * @return  A map containing examples of configurations that may be used for
   *          this extension.  It may be {@code null} or empty if there should
   *          not be any example argument sets.
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    // This SASL mechanism handler doesn't take any arguments.
    final LinkedHashMap<List<String>,String> exampleArgs =
         new LinkedHashMap<List<String>,String>(1);

    exampleArgs.put(
         new ArrayList<String>(0),
         "Configure the SASL mechanism handler without any arguments, as " +
              "none are needed for this implementation.");

    return exampleArgs;
  }



  /**
   * Indicates whether the specified SASL mechanism, which is listed as
   * supported by this handler, is available for use by the given user.
   *
   * @param  mechanism          The name of the SASL mechanism for which to make
   *                            the determination.  It will be one of the
   *                            mechanisms for which this handler is registered.
   * @param  passwordAttribute  The name of the attribute used to hold the
   *                            password for the user.
   * @param  userEntry          The entry for the user for whom to make the
   *                            determination.
   *
   * @return  {@code true} if the SASL mechanism is supported for the user,
   *          {@code false} if not, or {@code null} if it could not be
   *          determined.
   */
  @Override()
  public Boolean isMechanismAvailableForUser(final String mechanism,
                                             final String passwordAttribute,
                                             final Entry userEntry)
  {
    // Make sure that the user has a password.
    return userEntry.hasAttribute(passwordAttribute);
  }
}