001/*
002 * Copyright 2009-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2009-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.util.List;
041import javax.security.sasl.Sasl;
042import javax.security.sasl.SaslClient;
043
044import com.unboundid.asn1.ASN1OctetString;
045import com.unboundid.util.Debug;
046import com.unboundid.util.NotNull;
047import com.unboundid.util.Nullable;
048import com.unboundid.util.StaticUtils;
049import com.unboundid.util.ThreadSafety;
050import com.unboundid.util.ThreadSafetyLevel;
051
052import static com.unboundid.ldap.sdk.LDAPMessages.*;
053
054
055
056/**
057 * This class provides a mechanism for performing a SASL bind operation (or set
058 * of operations) using a Java {@code SaslClient} to perform all of the
059 * SASL-related processing.  This also supports enabling communication security
060 * for SASL mechanisms that support the {@code auth-int} or {@code auth-conf}
061 * quality of protection mechanisms.
062 */
063@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
064public final class SASLClientBindHandler
065{
066  // The set of controls to include in the request.
067  @Nullable private final Control[] controls;
068
069  // The message ID used when communicating with the directory server.
070  private volatile int messageID;
071
072  // The connection to use to communicate with the directory server.
073  @NotNull private final LDAPConnection connection;
074
075  // A list that will be updated with messages about any unhandled callbacks
076  // encountered during processing.
077  @NotNull private final List<String> unhandledCallbackMessages;
078
079  // The maximum length of time in milliseconds to wait for a response from the
080  // server.
081  private final long responseTimeoutMillis;
082
083  // The SASL bind request being processed.
084  @NotNull private final SASLBindRequest bindRequest;
085
086  // The SASL client to use to perform the processing.
087  @NotNull private final SaslClient saslClient;
088
089  // The name of the SASL mechanism to use.
090  @NotNull private final String mechanism;
091
092
093
094  /**
095   * Creates a new SASL client with the provided information.
096   *
097   * @param  bindRequest                The SASL bind request being processed.
098   *                                    This must not be {@code null}.
099   * @param  connection                 The connection to use to communicate
100   *                                    with the directory server.  This must
101   *                                    not be {@code null}.
102   * @param  mechanism                  The name of the SASL mechanism to use.
103   *                                    This must not be {@code null} or empty.
104   * @param  saslClient                 The Java SASL client instance to use to
105   *                                    perform the processing.  This must not
106   *                                    be {@code null}.
107   * @param  controls                   The set of controls to include in the
108   *                                    request.  This may be {@code null} or
109   *                                    empty if no controls should be included
110   *                                    in the request.
111   * @param  responseTimeoutMillis      The maximum length of time in
112   *                                    milliseconds to wait for a response from
113   *                                    the server.  A value that is less than
114   *                                    or equal to zero indicates that no
115   *                                    timeout should be enforced.
116   * @param  unhandledCallbackMessages  A list that will be updated with
117   *                                    messages about any unhandled callbacks.
118   *                                    This list must be managed by the bind
119   *                                    request class, which should update it if
120   *                                    its {@code CallbackHandler.handle}
121   *                                    method is invoked with one or more
122   *                                    callbacks that it does not handle or
123   *                                    support.  It must not be {@code null}.
124   */
125  public SASLClientBindHandler(
126             @NotNull final SASLBindRequest bindRequest,
127             @NotNull final LDAPConnection connection,
128             @NotNull final String mechanism,
129             @NotNull final SaslClient saslClient,
130             @Nullable final Control[] controls,
131             final long responseTimeoutMillis,
132             @NotNull final List<String> unhandledCallbackMessages)
133  {
134    this.bindRequest               = bindRequest;
135    this.connection                = connection;
136    this.mechanism                 = mechanism;
137    this.saslClient                = saslClient;
138    this.controls                  = controls;
139    this.responseTimeoutMillis     = responseTimeoutMillis;
140    this.unhandledCallbackMessages = unhandledCallbackMessages;
141
142    messageID = -1;
143  }
144
145
146
147  /**
148   * Performs a SASL bind against an LDAP directory server.
149   *
150   * @return  The result of the bind operation processing.
151   *
152   * @throws  LDAPException  If a problem occurs while processing the bind.
153   */
154  @NotNull()
155  public BindResult processSASLBind()
156         throws LDAPException
157  {
158    try
159    {
160      // Get the SASL credentials for the initial request.
161      byte[] credBytes = null;
162      try
163      {
164        if (saslClient.hasInitialResponse())
165        {
166          credBytes = saslClient.evaluateChallenge(new byte[0]);
167        }
168      }
169      catch (final Exception e)
170      {
171        Debug.debugException(e);
172        if (unhandledCallbackMessages.isEmpty())
173        {
174          throw new LDAPException(ResultCode.LOCAL_ERROR,
175               ERR_SASL_CANNOT_CREATE_INITIAL_REQUEST.get(mechanism,
176                    StaticUtils.getExceptionMessage(e)), e);
177        }
178        else
179        {
180          throw new LDAPException(ResultCode.LOCAL_ERROR,
181               ERR_SASL_CANNOT_CREATE_INITIAL_REQUEST_UNHANDLED_CALLBACKS.get(
182                    mechanism, StaticUtils.getExceptionMessage(e),
183                    StaticUtils.concatenateStrings(unhandledCallbackMessages)),
184               e);
185        }
186      }
187
188      ASN1OctetString saslCredentials;
189      if ((credBytes == null) || (credBytes.length == 0))
190      {
191        saslCredentials = null;
192      }
193      else
194      {
195        saslCredentials = new ASN1OctetString(credBytes);
196      }
197
198      BindResult bindResult = bindRequest.sendBindRequest(connection, "",
199           saslCredentials, controls, responseTimeoutMillis);
200      messageID = bindRequest.getLastMessageID();
201
202      if (! bindResult.getResultCode().equals(ResultCode.SASL_BIND_IN_PROGRESS))
203      {
204        return bindResult;
205      }
206
207      byte[] serverCredBytes;
208      ASN1OctetString serverCreds = bindResult.getServerSASLCredentials();
209      if (serverCreds == null)
210      {
211        serverCredBytes = null;
212      }
213      else
214      {
215        serverCredBytes = serverCreds.getValue();
216      }
217
218      while (true)
219      {
220        try
221        {
222          credBytes = saslClient.evaluateChallenge(serverCredBytes);
223        }
224        catch (final Exception e)
225        {
226          Debug.debugException(e);
227          if (unhandledCallbackMessages.isEmpty())
228          {
229            throw new LDAPException(ResultCode.LOCAL_ERROR,
230                 ERR_SASL_CANNOT_CREATE_SUBSEQUENT_REQUEST.get(mechanism,
231                      StaticUtils.getExceptionMessage(e)), e);
232          }
233          else
234          {
235            throw new LDAPException(ResultCode.LOCAL_ERROR,
236                 ERR_SASL_CANNOT_CREATE_SUBSEQUENT_REQUEST_UNHANDLED_CALLBACKS.
237                      get(mechanism, StaticUtils.getExceptionMessage(e),
238                           StaticUtils.concatenateStrings(
239                                unhandledCallbackMessages)),
240                 e);
241          }
242        }
243
244        // Create the bind request protocol op.
245        if ((credBytes == null) || (credBytes.length == 0))
246        {
247          saslCredentials = null;
248        }
249        else
250        {
251          saslCredentials = new ASN1OctetString(credBytes);
252        }
253
254        bindResult = bindRequest.sendBindRequest(connection, "",
255             saslCredentials, controls, responseTimeoutMillis);
256        messageID = bindRequest.getLastMessageID();
257        if (! bindResult.getResultCode().equals(
258                   ResultCode.SASL_BIND_IN_PROGRESS))
259        {
260          // Even if this is the final response, the server credentials may
261          // still have information useful to the SASL client (e.g., cipher
262          // information to use for applying quality of protection).  Feed that
263          // to the SASL client.
264          final ASN1OctetString serverCredentials =
265               bindResult.getServerSASLCredentials();
266          if (serverCredentials != null)
267          {
268            try
269            {
270              saslClient.evaluateChallenge(serverCredentials.getValue());
271            }
272            catch (final Exception e)
273            {
274              Debug.debugException(e);
275            }
276          }
277
278          return bindResult;
279        }
280
281        serverCreds = bindResult.getServerSASLCredentials();
282        if (serverCreds == null)
283        {
284          serverCredBytes = null;
285        }
286        else
287        {
288          serverCredBytes = serverCreds.getValue();
289        }
290      }
291    }
292    finally
293    {
294      boolean hasNegotiatedSecurity = false;
295      if (saslClient.isComplete())
296      {
297        final Object qopObject = saslClient.getNegotiatedProperty(Sasl.QOP);
298        if (qopObject != null)
299        {
300          final String qopString =
301               StaticUtils.toLowerCase(String.valueOf(qopObject));
302          if (qopString.contains(SASLQualityOfProtection.AUTH_INT.toString()) ||
303               qopString.contains(SASLQualityOfProtection.AUTH_CONF.toString()))
304          {
305            hasNegotiatedSecurity = true;
306          }
307        }
308      }
309
310      if (hasNegotiatedSecurity)
311      {
312        connection.applySASLSecurityLayer(saslClient);
313      }
314      else
315      {
316        try
317        {
318          saslClient.dispose();
319        }
320        catch (final Exception e)
321        {
322          Debug.debugException(e);
323        }
324      }
325    }
326  }
327
328
329
330  /**
331   * Retrieves the message ID for the last message in the exchange with the
332   * directory server.
333   *
334   * @return  The message for the last message in the exchange with the
335   *          directory server.
336   */
337  public int getMessageID()
338  {
339    return messageID;
340  }
341}