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}