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