/* * 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); } }