001/*
002 * Copyright 2013-2022 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2013-2022 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-2022 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    messageID = InternalSDKHelper.nextMessageID(connection);
323    return sendBindRequest(connection, "",
324         encodeCredentials(authenticationID, authorizationID, oneTimePassword),
325         getControls(), getResponseTimeoutMillis(connection));
326  }
327
328
329
330  /**
331   * Encodes the provided information into an ASN.1 octet string that may be
332   * used as the SASL credentials for an UnboundID delivered one-time password
333   * bind request.
334   *
335   * @param  authenticationID  The authentication identity for the bind request.
336   *                           It must not be {@code null} and must in the form
337   *                           "u:" followed by a username, or "dn:" followed
338   *                           by a DN.
339   * @param  authorizationID   The authorization identity for the bind request.
340   *                           It may be {@code null} if the authorization
341   *                           identity should be the same as the authentication
342   *                           identity.  If an authorization identity is
343   *                           specified, it must be in the form "u:" followed
344   *                           by a username, or "dn:" followed by a DN.  The
345   *                           value "dn:" may be used to indicate the
346   *                           authorization identity of the anonymous user.
347   * @param  oneTimePassword   The one-time password that has been delivered to
348   *                           the user via the deliver one-time password
349   *                           extended request.  It must not be {@code null}.
350   *
351   * @return  An ASN.1 octet string that may be used as the SASL credentials for
352   *          an UnboundID delivered one-time password bind request.
353   */
354  @NotNull()
355  public static ASN1OctetString encodeCredentials(
356                                     @NotNull final String authenticationID,
357                                     @Nullable final String authorizationID,
358                                     @NotNull final String oneTimePassword)
359  {
360    Validator.ensureNotNull(authenticationID);
361    Validator.ensureNotNull(oneTimePassword);
362
363    final ArrayList<ASN1Element> elements = new ArrayList<>(3);
364    elements.add(new ASN1OctetString(TYPE_AUTHENTICATION_ID, authenticationID));
365
366    if (authorizationID != null)
367    {
368      elements.add(new ASN1OctetString(TYPE_AUTHORIZATION_ID, authorizationID));
369    }
370
371    elements.add(new ASN1OctetString(TYPE_OTP, oneTimePassword));
372    return new ASN1OctetString(new ASN1Sequence(elements).encode());
373  }
374
375
376
377  /**
378   * {@inheritDoc}
379   */
380  @Override()
381  @NotNull()
382  public UnboundIDDeliveredOTPBindRequest duplicate()
383  {
384    return duplicate(getControls());
385  }
386
387
388
389  /**
390   * {@inheritDoc}
391   */
392  @Override()
393  @NotNull()
394  public UnboundIDDeliveredOTPBindRequest duplicate(
395              @Nullable final Control[] controls)
396  {
397    final UnboundIDDeliveredOTPBindRequest bindRequest =
398         new UnboundIDDeliveredOTPBindRequest(authenticationID,
399              authorizationID, oneTimePassword, controls);
400    bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
401    return bindRequest;
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  @NotNull()
411  public String getSASLMechanismName()
412  {
413    return UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME;
414  }
415
416
417
418  /**
419   * {@inheritDoc}
420   */
421  @Override()
422  public int getLastMessageID()
423  {
424    return messageID;
425  }
426
427
428
429  /**
430   * {@inheritDoc}
431   */
432  @Override()
433  public void toString(@NotNull final StringBuilder buffer)
434  {
435    buffer.append("UnboundDeliveredOTPBindRequest(authID='");
436    buffer.append(authenticationID);
437    buffer.append("', ");
438
439    if (authorizationID != null)
440    {
441      buffer.append("authzID='");
442      buffer.append(authorizationID);
443      buffer.append("', ");
444    }
445
446    final Control[] controls = getControls();
447    if (controls.length > 0)
448    {
449      buffer.append(", controls={");
450      for (int i=0; i < controls.length; i++)
451      {
452        if (i > 0)
453        {
454          buffer.append(", ");
455        }
456
457        buffer.append(controls[i]);
458      }
459      buffer.append('}');
460    }
461
462    buffer.append(')');
463  }
464
465
466
467  /**
468   * {@inheritDoc}
469   */
470  @Override()
471  public void toCode(@NotNull final List<String> lineList,
472                     @NotNull final String requestID,
473                     final int indentSpaces, final boolean includeProcessing)
474  {
475    // Create the request variable.
476    final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(4);
477    constructorArgs.add(ToCodeArgHelper.createString(authenticationID,
478         "Authentication ID"));
479    constructorArgs.add(ToCodeArgHelper.createString(authorizationID,
480         "Authorization ID"));
481    constructorArgs.add(ToCodeArgHelper.createString("---redacted-otp---",
482         "One-Time Password"));
483
484    final Control[] controls = getControls();
485    if (controls.length > 0)
486    {
487      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
488           "Bind Controls"));
489    }
490
491    ToCodeHelper.generateMethodCall(lineList, indentSpaces,
492         "UnboundIDDeliveredOTPBindRequest", requestID + "Request",
493         "new UnboundIDDeliveredOTPBindRequest", constructorArgs);
494
495
496    // Add lines for processing the request and obtaining the result.
497    if (includeProcessing)
498    {
499      // Generate a string with the appropriate indent.
500      final StringBuilder buffer = new StringBuilder();
501      for (int i=0; i < indentSpaces; i++)
502      {
503        buffer.append(' ');
504      }
505      final String indent = buffer.toString();
506
507      lineList.add("");
508      lineList.add(indent + "try");
509      lineList.add(indent + '{');
510      lineList.add(indent + "  BindResult " + requestID +
511           "Result = connection.bind(" + requestID + "Request);");
512      lineList.add(indent + "  // The bind was processed successfully.");
513      lineList.add(indent + '}');
514      lineList.add(indent + "catch (LDAPException e)");
515      lineList.add(indent + '{');
516      lineList.add(indent + "  // The bind failed.  Maybe the following will " +
517           "help explain why.");
518      lineList.add(indent + "  // Note that the connection is now likely in " +
519           "an unauthenticated state.");
520      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
521      lineList.add(indent + "  String message = e.getMessage();");
522      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
523      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
524      lineList.add(indent + "  Control[] responseControls = " +
525           "e.getResponseControls();");
526      lineList.add(indent + '}');
527    }
528  }
529}