001/*
002 * Copyright 2013-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2013-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) 2013-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.unboundidds;
037
038
039
040import java.util.ArrayList;
041import java.util.List;
042
043import com.unboundid.asn1.ASN1Element;
044import com.unboundid.asn1.ASN1OctetString;
045import com.unboundid.asn1.ASN1Sequence;
046import com.unboundid.ldap.sdk.BindResult;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.InternalSDKHelper;
049import com.unboundid.ldap.sdk.LDAPConnection;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.ldap.sdk.SASLBindRequest;
053import com.unboundid.ldap.sdk.ToCodeArgHelper;
054import com.unboundid.ldap.sdk.ToCodeHelper;
055import com.unboundid.ldap.sdk.unboundidds.extensions.
056            DeliverOneTimePasswordExtendedRequest;
057import com.unboundid.util.Debug;
058import com.unboundid.util.NotMutable;
059import com.unboundid.util.NotNull;
060import com.unboundid.util.Nullable;
061import com.unboundid.util.StaticUtils;
062import com.unboundid.util.ThreadSafety;
063import com.unboundid.util.ThreadSafetyLevel;
064import com.unboundid.util.Validator;
065
066import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
067
068
069
070/**
071 * This class provides support for an UnboundID-proprietary SASL mechanism that
072 * allows for multifactor authentication using a one-time password that has been
073 * delivered to the user via some out-of-band mechanism as triggered by the
074 * {@link DeliverOneTimePasswordExtendedRequest} (which requires the user to
075 * provide an authentication ID and a static password).
076 * <BR>
077 * <BLOCKQUOTE>
078 *   <B>NOTE:</B>  This class, and other classes within the
079 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
080 *   supported for use against Ping Identity, UnboundID, and
081 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
082 *   for proprietary functionality or for external specifications that are not
083 *   considered stable or mature enough to be guaranteed to work in an
084 *   interoperable way with other types of LDAP servers.
085 * </BLOCKQUOTE>
086 * <BR>
087 * The name for this SASL mechanism is "UNBOUNDID-DELIVERED-OTP".  An
088 * UNBOUNDID-DELIVERED-OTP SASL bind request MUST include SASL credentials with
089 * the following ASN.1 encoding:
090 * <BR><BR>
091 * <PRE>
092 *   UnboundIDDeliveredOTPCredentials ::= SEQUENCE {
093 *        authenticationID     [0] OCTET STRING,
094 *        authorizationID      [1] OCTET STRING OPTIONAL.
095 *        oneTimePassword      [2] OCTET STRING,
096 *        ... }
097 * </PRE>
098 */
099@NotMutable()
100@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
101public final class UnboundIDDeliveredOTPBindRequest
102       extends SASLBindRequest
103{
104  /**
105   * The name for the UnboundID delivered OTP SASL mechanism.
106   */
107  @NotNull public static final String UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME =
108       "UNBOUNDID-DELIVERED-OTP";
109
110
111
112  /**
113   * The BER type for the authentication ID included in the request.
114   */
115  private static final byte TYPE_AUTHENTICATION_ID = (byte) 0x80;
116
117
118
119  /**
120   * The BER type for the authorization ID included in the request.
121   */
122  private static final byte TYPE_AUTHORIZATION_ID = (byte) 0x81;
123
124
125
126  /**
127   * The BER type for the one-time password included in the request.
128   */
129  private static final byte TYPE_OTP = (byte) 0x82;
130
131
132
133  /**
134   * The serial version UID for this serializable class.
135   */
136  private static final long serialVersionUID = 8148101285676071058L;
137
138
139
140  // The message ID from the last LDAP message sent from this request.
141  private volatile int messageID = -1;
142
143  // The authentication identity for the bind.
144  @NotNull private final String authenticationID;
145
146  // The authorization identity for the bind, if provided.
147  @Nullable private final String authorizationID;
148
149  // The one-time password for the bind, if provided.
150  @NotNull private final String oneTimePassword;
151
152
153
154  /**
155   * Creates a new delivered one-time password bind request with the provided
156   * information.
157   *
158   * @param  authenticationID  The authentication identity for the bind request.
159   *                           It must not be {@code null} and must in the form
160   *                           "u:" followed by a username, or "dn:" followed
161   *                           by a DN.
162   * @param  authorizationID   The authorization identity for the bind request.
163   *                           It may be {@code null} if the authorization
164   *                           identity should be the same as the authentication
165   *                           identity.  If an authorization identity is
166   *                           specified, it must be in the form "u:" followed
167   *                           by a username, or "dn:" followed by a DN.  The
168   *                           value "dn:" may be used to indicate the
169   *                           authorization identity of the anonymous user.
170   * @param  oneTimePassword   The one-time password that has been delivered to
171   *                           the user via the deliver one-time password
172   *                           extended request.  It must not be {@code null}.
173   * @param  controls          The set of controls to include in the bind
174   *                           request.  It may be {@code null} or empty if no
175   *                           controls should be included.
176   */
177  public UnboundIDDeliveredOTPBindRequest(
178              @NotNull final String authenticationID,
179              @Nullable final String authorizationID,
180              @NotNull final String oneTimePassword,
181              @Nullable final Control... controls)
182  {
183    super(controls);
184
185    Validator.ensureNotNull(authenticationID);
186    Validator.ensureNotNull(oneTimePassword);
187
188    this.authenticationID = authenticationID;
189    this.authorizationID = authorizationID;
190    this.oneTimePassword  = oneTimePassword;
191  }
192
193
194
195  /**
196   * Creates a new delivered one-time password bind request from the information
197   * contained in the provided encoded SASL credentials.
198   *
199   * @param  saslCredentials  The encoded SASL credentials to be decoded in
200   *                          order to create this delivered one-time password
201   *                          bind request.  It must not be {@code null}.
202   * @param  controls         The set of controls to include in the bind
203   *                          request.  It may be {@code null} or empty if no
204   *                          controls should be included.
205   *
206   * @return  The delivered one-time password bind request decoded from the
207   *          provided credentials.
208   *
209   * @throws  LDAPException  If the provided credentials are not valid for an
210   *                         UNBOUNDID-DELIVERED-OTP bind request.
211   */
212  @NotNull()
213  public static UnboundIDDeliveredOTPBindRequest decodeSASLCredentials(
214                     @NotNull final ASN1OctetString saslCredentials,
215                     @Nullable final Control... controls)
216         throws LDAPException
217  {
218    String          authenticationID = null;
219    String          authorizationID  = null;
220    String          oneTimePassword  = null;
221
222    try
223    {
224      final ASN1Sequence s =
225           ASN1Sequence.decodeAsSequence(saslCredentials.getValue());
226      for (final ASN1Element e : s.elements())
227      {
228        switch (e.getType())
229        {
230          case TYPE_AUTHENTICATION_ID:
231            authenticationID = e.decodeAsOctetString().stringValue();
232            break;
233          case TYPE_AUTHORIZATION_ID:
234            authorizationID = e.decodeAsOctetString().stringValue();
235            break;
236          case TYPE_OTP:
237            oneTimePassword = e.decodeAsOctetString().stringValue();
238            break;
239          default:
240            throw new LDAPException(ResultCode.DECODING_ERROR,
241                 ERR_DOTP_DECODE_INVALID_ELEMENT_TYPE.get(
242                      StaticUtils.toHex(e.getType())));
243        }
244      }
245    }
246    catch (final Exception e)
247    {
248      Debug.debugException(e);
249      throw new LDAPException(ResultCode.DECODING_ERROR,
250           ERR_DOTP_DECODE_ERROR.get(StaticUtils.getExceptionMessage(e)),
251           e);
252    }
253
254    if (authenticationID == null)
255    {
256      throw new LDAPException(ResultCode.DECODING_ERROR,
257           ERR_DOTP_DECODE_MISSING_AUTHN_ID.get());
258    }
259
260    if (oneTimePassword == null)
261    {
262      throw new LDAPException(ResultCode.DECODING_ERROR,
263           ERR_DOTP_DECODE_MISSING_OTP.get());
264    }
265
266    return new UnboundIDDeliveredOTPBindRequest(authenticationID,
267         authorizationID, oneTimePassword, controls);
268  }
269
270
271
272  /**
273   * Retrieves the authentication identity for the bind request.
274   *
275   * @return  The authentication identity for the bind request.
276   */
277  @NotNull()
278  public String getAuthenticationID()
279  {
280    return authenticationID;
281  }
282
283
284
285  /**
286   * Retrieves the authorization identity for the bind request, if available.
287   *
288   * @return  The authorization identity for the bind request, or {@code null}
289   *          if the authorization identity should be the same as the
290   *          authentication identity.
291   */
292  @Nullable()
293  public String getAuthorizationID()
294  {
295    return authorizationID;
296  }
297
298
299
300  /**
301   * Retrieves the one-time password for the bind request.
302   *
303   * @return  The one-time password for the bind request.
304   */
305  @NotNull()
306  public String getOneTimePassword()
307  {
308    return oneTimePassword;
309  }
310
311
312
313  /**
314   * {@inheritDoc}
315   */
316  @Override()
317  @NotNull()
318  protected BindResult process(@NotNull final LDAPConnection connection,
319                               final int depth)
320            throws LDAPException
321  {
322    setReferralDepth(depth);
323
324    messageID = InternalSDKHelper.nextMessageID(connection);
325    return sendBindRequest(connection, "",
326         encodeCredentials(authenticationID, authorizationID, oneTimePassword),
327         getControls(), getResponseTimeoutMillis(connection));
328  }
329
330
331
332  /**
333   * Encodes the provided information into an ASN.1 octet string that may be
334   * used as the SASL credentials for an UnboundID delivered one-time password
335   * bind request.
336   *
337   * @param  authenticationID  The authentication identity for the bind request.
338   *                           It must not be {@code null} and must in the form
339   *                           "u:" followed by a username, or "dn:" followed
340   *                           by a DN.
341   * @param  authorizationID   The authorization identity for the bind request.
342   *                           It may be {@code null} if the authorization
343   *                           identity should be the same as the authentication
344   *                           identity.  If an authorization identity is
345   *                           specified, it must be in the form "u:" followed
346   *                           by a username, or "dn:" followed by a DN.  The
347   *                           value "dn:" may be used to indicate the
348   *                           authorization identity of the anonymous user.
349   * @param  oneTimePassword   The one-time password that has been delivered to
350   *                           the user via the deliver one-time password
351   *                           extended request.  It must not be {@code null}.
352   *
353   * @return  An ASN.1 octet string that may be used as the SASL credentials for
354   *          an UnboundID delivered one-time password bind request.
355   */
356  @NotNull()
357  public static ASN1OctetString encodeCredentials(
358                                     @NotNull final String authenticationID,
359                                     @Nullable final String authorizationID,
360                                     @NotNull final String oneTimePassword)
361  {
362    Validator.ensureNotNull(authenticationID);
363    Validator.ensureNotNull(oneTimePassword);
364
365    final ArrayList<ASN1Element> elements = new ArrayList<>(3);
366    elements.add(new ASN1OctetString(TYPE_AUTHENTICATION_ID, authenticationID));
367
368    if (authorizationID != null)
369    {
370      elements.add(new ASN1OctetString(TYPE_AUTHORIZATION_ID, authorizationID));
371    }
372
373    elements.add(new ASN1OctetString(TYPE_OTP, oneTimePassword));
374    return new ASN1OctetString(new ASN1Sequence(elements).encode());
375  }
376
377
378
379  /**
380   * {@inheritDoc}
381   */
382  @Override()
383  @NotNull()
384  public UnboundIDDeliveredOTPBindRequest duplicate()
385  {
386    return duplicate(getControls());
387  }
388
389
390
391  /**
392   * {@inheritDoc}
393   */
394  @Override()
395  @NotNull()
396  public UnboundIDDeliveredOTPBindRequest duplicate(
397              @Nullable final Control[] controls)
398  {
399    final UnboundIDDeliveredOTPBindRequest bindRequest =
400         new UnboundIDDeliveredOTPBindRequest(authenticationID,
401              authorizationID, oneTimePassword, controls);
402    bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
403    bindRequest.setIntermediateResponseListener(
404         getIntermediateResponseListener());
405    bindRequest.setReferralDepth(getReferralDepth());
406    bindRequest.setReferralConnector(getReferralConnectorInternal());
407    return bindRequest;
408  }
409
410
411
412  /**
413   * {@inheritDoc}
414   */
415  @Override()
416  @NotNull()
417  public String getSASLMechanismName()
418  {
419    return UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME;
420  }
421
422
423
424  /**
425   * {@inheritDoc}
426   */
427  @Override()
428  public int getLastMessageID()
429  {
430    return messageID;
431  }
432
433
434
435  /**
436   * {@inheritDoc}
437   */
438  @Override()
439  public void toString(@NotNull final StringBuilder buffer)
440  {
441    buffer.append("UnboundDeliveredOTPBindRequest(authID='");
442    buffer.append(authenticationID);
443    buffer.append("', ");
444
445    if (authorizationID != null)
446    {
447      buffer.append("authzID='");
448      buffer.append(authorizationID);
449      buffer.append("', ");
450    }
451
452    final Control[] controls = getControls();
453    if (controls.length > 0)
454    {
455      buffer.append(", controls={");
456      for (int i=0; i < controls.length; i++)
457      {
458        if (i > 0)
459        {
460          buffer.append(", ");
461        }
462
463        buffer.append(controls[i]);
464      }
465      buffer.append('}');
466    }
467
468    buffer.append(')');
469  }
470
471
472
473  /**
474   * {@inheritDoc}
475   */
476  @Override()
477  public void toCode(@NotNull final List<String> lineList,
478                     @NotNull final String requestID,
479                     final int indentSpaces, final boolean includeProcessing)
480  {
481    // Create the request variable.
482    final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(4);
483    constructorArgs.add(ToCodeArgHelper.createString(authenticationID,
484         "Authentication ID"));
485    constructorArgs.add(ToCodeArgHelper.createString(authorizationID,
486         "Authorization ID"));
487    constructorArgs.add(ToCodeArgHelper.createString("---redacted-otp---",
488         "One-Time Password"));
489
490    final Control[] controls = getControls();
491    if (controls.length > 0)
492    {
493      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
494           "Bind Controls"));
495    }
496
497    ToCodeHelper.generateMethodCall(lineList, indentSpaces,
498         "UnboundIDDeliveredOTPBindRequest", requestID + "Request",
499         "new UnboundIDDeliveredOTPBindRequest", constructorArgs);
500
501
502    // Add lines for processing the request and obtaining the result.
503    if (includeProcessing)
504    {
505      // Generate a string with the appropriate indent.
506      final StringBuilder buffer = new StringBuilder();
507      for (int i=0; i < indentSpaces; i++)
508      {
509        buffer.append(' ');
510      }
511      final String indent = buffer.toString();
512
513      lineList.add("");
514      lineList.add(indent + "try");
515      lineList.add(indent + '{');
516      lineList.add(indent + "  BindResult " + requestID +
517           "Result = connection.bind(" + requestID + "Request);");
518      lineList.add(indent + "  // The bind was processed successfully.");
519      lineList.add(indent + '}');
520      lineList.add(indent + "catch (LDAPException e)");
521      lineList.add(indent + '{');
522      lineList.add(indent + "  // The bind failed.  Maybe the following will " +
523           "help explain why.");
524      lineList.add(indent + "  // Note that the connection is now likely in " +
525           "an unauthenticated state.");
526      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
527      lineList.add(indent + "  String message = e.getMessage();");
528      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
529      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
530      lineList.add(indent + "  Control[] responseControls = " +
531           "e.getResponseControls();");
532      lineList.add(indent + '}');
533    }
534  }
535}