001/*
002 * Copyright 2023-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2023-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) 2023-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.controls;
037
038
039
040import java.util.Date;
041import java.util.LinkedHashMap;
042import java.util.List;
043import java.util.Map;
044
045import com.unboundid.asn1.ASN1OctetString;
046import com.unboundid.ldap.sdk.BindResult;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.DecodeableControl;
049import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.util.Debug;
053import com.unboundid.util.NotMutable;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.ObjectTrio;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.json.JSONField;
061import com.unboundid.util.json.JSONObject;
062import com.unboundid.util.json.JSONString;
063import com.unboundid.util.json.JSONValue;
064
065import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
066
067
068
069/**
070 * This class provides a response control that may be used to convey the
071 * access token (and other associated information) generated in response to a
072 * {@link GenerateAccessTokenRequestControl} for a successful bind operation.
073 * <BR>
074 * <BLOCKQUOTE>
075 *   <B>NOTE:</B>  This class, and other classes within the
076 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
077 *   supported for use against Ping Identity, UnboundID, and
078 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
079 *   for proprietary functionality or for external specifications that are not
080 *   considered stable or mature enough to be guaranteed to work in an
081 *   interoperable way with other types of LDAP servers.
082 * </BLOCKQUOTE>
083 * <BR>
084 * This control has an OID of "1.3.6.1.4.1.30221.2.5.68", a criticality of
085 * false, and a value that is the string representation of a JSON object with
086 * the following fields:
087 * <UL>
088 *   <LI>
089 *     {@code token} -- The access token that was generated by the server.  This
090 *     field may be absent if an error occurred while attempting to generate the
091 *     access token.
092 *   </LI>
093 *   <LI>
094 *     {@code expiration-time} -- The time that the access token is expected to
095 *     expire.  If present, it will be formatted in the ISO 8601 format
096 *     described in RFC 3339 (which may be decoded using the
097 *     {@link StaticUtils#decodeRFC3339Time} method).  If absent, then the
098 *     access token may not expire.
099 *   </LI>
100 *   <LI>
101 *     {@code error-message} -- An optional message that may explain the reason
102 *     that an access token could not be generated for the request.
103 *   </LI>
104 * </UL>
105 *
106 * @see  GenerateAccessTokenRequestControl
107 */
108@NotMutable()
109@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
110public final class GenerateAccessTokenResponseControl
111       extends Control
112       implements DecodeableControl
113{
114  /**
115   * The OID (1.3.6.1.4.1.30221.2.5.68) for the generate access token response
116   * control.
117   */
118  @NotNull public static final String GENERATE_ACCESS_TOKEN_RESPONSE_OID =
119       "1.3.6.1.4.1.30221.2.5.68";
120
121
122
123  /**
124   * The name of the field used to hold the generated access token in the value
125   * of this control.
126   */
127  @NotNull private static final String JSON_FIELD_ACCESS_TOKEN = "token";
128
129
130
131  /**
132   * The name of the field used to hold the error message in the value of this
133   * control.
134   */
135  @NotNull private static final String JSON_FIELD_ERROR_MESSAGE =
136       "error-message";
137
138
139
140  /**
141   * The name of the field used to hold the access token expiration time in the
142   * value of this control.
143   */
144  @NotNull private static final String JSON_FIELD_EXPIRATION_TIME =
145       "expiration-time";
146
147
148
149  /**
150   * The serial version UID for this serializable class.
151   */
152  private static final long serialVersionUID = -6071943602038789356L;
153
154
155
156  // The access token expiration time included in the control.
157  @Nullable private final Long expirationTime;
158
159  // The generated access token included in the control.
160  @Nullable private final String accesToken;
161
162  // The error message included in the control.
163  @Nullable private final String errorMessage;
164
165
166
167  /**
168   * Creates a new empty control instance that is intended to be used only for
169   * decoding controls via the {@code DecodeableControl} interface.
170   */
171  GenerateAccessTokenResponseControl()
172  {
173    expirationTime = null;
174    accesToken = null;
175    errorMessage = null;
176  }
177
178
179
180  /**
181   * Creates a new generate access token response control with the provided
182   * information.
183   *
184   * @param  accessToken     The access token that was generated.  It may be
185   *                         {@code null} if no access token was generated.
186   * @param  expirationTime  The time that the access token is expected to
187   *                         expire.  It may be {@code null} if no access token
188   *                         was generated, or if the token does not have an
189   *                         expiration time.
190   * @param  errorMessage    An error message with the reason the access token
191   *                         was not generated.  It may be {@code null} if the
192   *                         access token was generated successfully or if no
193   *                         error message is available.
194   */
195  public GenerateAccessTokenResponseControl(
196              @Nullable final String accessToken,
197              @Nullable final Date expirationTime,
198              @Nullable final String errorMessage)
199  {
200    super(GENERATE_ACCESS_TOKEN_RESPONSE_OID, false,
201         new ASN1OctetString(encodeValueObject(accessToken, expirationTime,
202              errorMessage).toString()));
203
204    this.accesToken = accessToken;
205    this.errorMessage = errorMessage;
206
207    if (expirationTime == null)
208    {
209      this.expirationTime = null;
210    }
211    else
212    {
213      this.expirationTime = expirationTime.getTime();
214    }
215  }
216
217
218
219  /**
220   * Creates a new generate access token response control with the provided
221   * information.
222   *
223   * @param  oid         The OID for the control.
224   * @param  isCritical  Indicates whether the control should be marked
225   *                     critical.
226   * @param  value       The encoded value for the control.  This may be
227   *                     {@code null} if no value was provided.
228   *
229   * @throws  LDAPException  If the provided control cannot be decoded as a
230   *                         generate access token response control.
231   */
232  public GenerateAccessTokenResponseControl(
233              @NotNull final String oid,
234              final boolean isCritical,
235              @Nullable final ASN1OctetString value)
236         throws LDAPException
237  {
238    super(oid, isCritical,  value);
239
240    if (value == null)
241    {
242      throw new LDAPException(ResultCode.DECODING_ERROR,
243           ERR_GENERATE_ACCESS_TOKEN_RESPONSE_NO_VALUE.get());
244    }
245
246    try
247    {
248      final JSONObject valueObject =  new JSONObject(value.stringValue());
249      final ObjectTrio<String,Date,String> valueElements =
250           decodeJSONObject(valueObject);
251      accesToken = valueElements.getFirst();
252      errorMessage = valueElements.getThird();
253
254      final Date expirationTimeDate = valueElements.getSecond();
255      if (expirationTimeDate == null)
256      {
257        expirationTime = null;
258      }
259      else
260      {
261        expirationTime = expirationTimeDate.getTime();
262      }
263    }
264    catch (final Exception e)
265    {
266      Debug.debugException(e);
267      throw new LDAPException(ResultCode.DECODING_ERROR,
268           ERR_GENERATE_ACCESS_TOKEN_RESPONSE_CANNOT_DECODE_VALUE.get(
269                StaticUtils.getExceptionMessage(e)),
270           e);
271    }
272  }
273
274
275
276  /**
277   * Decodes the provided JSON object as the value of a generate access token
278   * response control.
279   *
280   * @param  valueObject  The JSON object to use to decode the value of a
281   *                      generate access token response control.  It must not
282   *                      be {@code null}.
283   *
284   * @return  An {@code ObjectTrio} in which the first element is the access
285   *          token, the second element is the expiration time, and the third
286   *          element is the error message.  Any or all of the elements may be
287   *          {@code null}.
288   *
289   * @throws  LDAPException  If a problem occurs while attempting to decode the
290   *                         JSON object as a generate access token value.
291   */
292  @NotNull()
293  private static ObjectTrio<String,Date,String> decodeJSONObject(
294               @NotNull final JSONObject valueObject)
295          throws LDAPException
296  {
297    final String accessToken =
298         valueObject.getFieldAsString(JSON_FIELD_ACCESS_TOKEN);
299    final String errorMessage =
300         valueObject.getFieldAsString(JSON_FIELD_ERROR_MESSAGE);
301
302    final Date expirationTime;
303    final String expirationTimeStr =
304         valueObject.getFieldAsString(JSON_FIELD_EXPIRATION_TIME);
305    if (expirationTimeStr == null)
306    {
307      expirationTime = null;
308    }
309    else
310    {
311      try
312      {
313        expirationTime = StaticUtils.decodeRFC3339Time(expirationTimeStr);
314      }
315      catch (final Exception e)
316      {
317        Debug.debugException(e);
318        throw new LDAPException(ResultCode.DECODING_ERROR,
319             ERR_GENERATE_ACCESS_TOKEN_RESPONSE_INVALID_TIMESTAMP.get(
320                  valueObject.toSingleLineString(),
321                  JSON_FIELD_EXPIRATION_TIME));
322      }
323    }
324
325    return new ObjectTrio<>(accessToken, expirationTime, errorMessage);
326  }
327
328
329
330  /**
331   * {@inheritDoc}
332   */
333  @Override()
334  @NotNull()
335  public GenerateAccessTokenResponseControl decodeControl(
336              @NotNull final String oid,
337              final boolean isCritical,
338              @Nullable final ASN1OctetString value)
339         throws LDAPException
340  {
341    return new GenerateAccessTokenResponseControl(oid, isCritical, value);
342  }
343
344
345
346  /**
347   * Extracts a generate access token response control from the provided result.
348   *
349   * @param  result  The result from which to retrieve the generate access token
350   *                 response control.
351   *
352   * @return  The generate access token response control contained in the
353   *          provided result, or {@code null} if the result did not contain a
354   *          generate access token response control.
355   *
356   * @throws  LDAPException  If a problem is encountered while attempting to
357   *                         decode the generate access token response control
358   *                         contained in the provided result.
359   */
360  @Nullable()
361  public static GenerateAccessTokenResponseControl get(
362              @NotNull final BindResult result)
363         throws LDAPException
364  {
365    final Control c =
366         result.getResponseControl(GENERATE_ACCESS_TOKEN_RESPONSE_OID);
367    if (c == null)
368    {
369      return null;
370    }
371
372    if (c instanceof GenerateAccessTokenResponseControl)
373    {
374      return (GenerateAccessTokenResponseControl) c;
375    }
376    else
377    {
378      return new GenerateAccessTokenResponseControl(c.getOID(), c.isCritical(),
379           c.getValue());
380    }
381  }
382
383
384
385  /**
386   * Encodes the provided information into a JSON object suitable for use in
387   * the value of this control.
388   *
389   * @param  accessToken     The access token that was generated.  It may be
390   *                         {@code null} if no access token was generated.
391   * @param  expirationTime  The time that the access token is expected to
392   *                         expire.  It may be {@code null} if no access token
393   *                         was generated, or if the token does not have an
394   *                         expiration time.
395   * @param  errorMessage    An error message containing the reason the access
396   *                         token was not generated.  It may be {@code null} if
397   *                         the access token was generated successfully or if
398   *                         no error message is available.
399   *
400   * @return  A JSON object containing the encoded control value information.
401   */
402  @NotNull()
403  private static JSONObject encodeValueObject(
404               @Nullable final String accessToken,
405               @Nullable final Date expirationTime,
406               @Nullable final String errorMessage)
407  {
408    return encodeValueObject(accessToken,
409         ((expirationTime == null) ? null : expirationTime.getTime()),
410         errorMessage);
411  }
412
413
414
415  /**
416   * Encodes the provided information into a JSON object suitable for use in
417   * the value of this control.
418   *
419   * @param  accessToken     The access token that was generated.  It may be
420   *                         {@code null} if no access token was generated.
421   * @param  expirationTime  The time that the access token is expected to
422   *                         expire.  It may be {@code null} if no access token
423   *                         was generated, or if the token does not have an
424   *                         expiration time.
425   * @param  errorMessage    An error message containing the reason the access
426   *                         token was not generated.  It may be {@code null} if
427   *                         the access token was generated successfully or if
428   *                         no error message is available.
429   *
430   * @return  A JSON object containing the encoded control value information.
431   */
432  @NotNull()
433  private static JSONObject encodeValueObject(
434               @Nullable final String accessToken,
435               @Nullable final Long expirationTime,
436               @Nullable final String errorMessage)
437  {
438    final Map<String,JSONValue> fields =
439         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
440
441    if (accessToken != null)
442    {
443      fields.put(JSON_FIELD_ACCESS_TOKEN, new JSONString(accessToken));
444    }
445
446    if (expirationTime != null)
447    {
448      fields.put(JSON_FIELD_EXPIRATION_TIME,
449           new JSONString(StaticUtils.encodeRFC3339Time(expirationTime)));
450    }
451
452    if (errorMessage != null)
453    {
454      fields.put(JSON_FIELD_ERROR_MESSAGE, new JSONString(errorMessage));
455    }
456
457    return new JSONObject(fields);
458  }
459
460
461
462  /**
463   * Retrieves the access token that was generated by the server.
464   *
465   * @return  The access token that was generated by the server, or {@code null}
466   *          if no access token was generated..
467   */
468  @Nullable()
469  public String getAccessToken()
470  {
471    return accesToken;
472  }
473
474
475
476  /**
477   * Retrieves the time that the generated access token is expected to expire.
478   *
479   * @return  The time that the generated access token is expected to expire, or
480   *          {@code null} if no access token was generated or if it does not
481   *          have an expiration time.
482   */
483  @Nullable()
484  public Date getExpirationTime()
485  {
486    if (expirationTime == null)
487    {
488      return null;
489    }
490    else
491    {
492      return new Date(expirationTime);
493    }
494  }
495
496
497
498  /**
499   * Retrieves an error message with the reason the access token was not
500   * generated.
501   *
502   * @return  An error message with the reason the access token was not
503   *          generated, or {@code null} if the access token was generated
504   *          successfully or if no error message is available.
505   */
506  @Nullable()
507  public String getErrorMessage()
508  {
509    return errorMessage;
510  }
511
512
513
514  /**
515   * {@inheritDoc}
516   */
517  @Override()
518  @NotNull()
519  public String getControlName()
520  {
521    return INFO_CONTROL_NAME_GENERATE_ACCESS_TOKEN_RESPONSE.get();
522  }
523
524
525
526  /**
527   * Retrieves a representation of this generate access token response control
528   * as a JSON object.  The JSON object uses the following fields:
529   * <UL>
530   *   <LI>
531   *     {@code oid} -- A mandatory string field whose value is the object
532   *     identifier for this control.  For the generate access token response
533   *     control, the OID is "1.3.6.1.4.1.30221.2.5.68".
534   *   </LI>
535   *   <LI>
536   *     {@code control-name} -- An optional string field whose value is a
537   *     human-readable name for this control.  This field is only intended for
538   *     descriptive purposes, and when decoding a control, the {@code oid}
539   *     field should be used to identify the type of control.
540   *   </LI>
541   *   <LI>
542   *     {@code criticality} -- A mandatory Boolean field used to indicate
543   *     whether this control is considered critical.
544   *   </LI>
545   *   <LI>
546   *     {@code value-base64} -- An optional string field whose value is a
547   *     base64-encoded representation of the raw value for this generate access
548   *     token response control.  Exactly one of the {@code value-base64} and
549   *     {@code value-json} fields must be present.
550   *   </LI>
551   *   <LI>
552   *     {@code value-json} -- An optional JSON object field whose value is a
553   *     user-friendly representation of the value for this generate access
554   *     token response control.  Exactly one of the {@code value-base64} and
555   *     {@code value-json} fields must be present, and if the
556   *     {@code value-json} field is used, then it will use the following
557   *     fields:
558   *     <UL>
559   *       <LI>
560   *         {@code token} -- An optional string field whose value is the access
561   *         token that was generated.
562   *       </LI>
563   *       <LI>
564   *         {@code expiration-time} -- An optional string field whose value is
565   *         a timestamp indicating the time that the access token will expire,
566   *         using the ISO 8601 format described in RFC 3339.
567   *       </LI>
568   *       <LI>
569   *         {@code error-message} -- An optional string field whose value is an
570   *         error message with the reason the access token was not generated.
571   *       </LI>
572   *     </UL>
573   *   </LI>
574   * </UL>
575   *
576   * @return  A JSON object that contains a representation of this control.
577   */
578  @Override()
579  @NotNull()
580  public JSONObject toJSONControl()
581  {
582    return new JSONObject(
583         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
584              GENERATE_ACCESS_TOKEN_RESPONSE_OID),
585         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
586              INFO_CONTROL_NAME_GENERATE_ACCESS_TOKEN_RESPONSE.get()),
587         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
588              isCritical()),
589         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
590              encodeValueObject(accesToken, expirationTime, errorMessage)));
591  }
592
593
594
595  /**
596   * Attempts to decode the provided object as a JSON representation of a
597   * generate access token response control.
598   *
599   * @param  controlObject  The JSON object to be decoded.  It must not be
600   *                        {@code null}.
601   * @param  strict         Indicates whether to use strict mode when decoding
602   *                        the provided JSON object.  If this is {@code true},
603   *                        then this method will throw an exception if the
604   *                        provided JSON object contains any unrecognized
605   *                        fields.  If this is {@code false}, then unrecognized
606   *                        fields will be ignored.
607   *
608   * @return  The generate access token response control that was decoded from
609   *          the provided JSON object.
610   *
611   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
612   *                         valid generate access token response control.
613   */
614  @NotNull()
615  public static GenerateAccessTokenResponseControl decodeJSONControl(
616              @NotNull final JSONObject controlObject,
617              final boolean strict)
618         throws LDAPException
619  {
620    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
621         controlObject, strict, true, true);
622
623    final ASN1OctetString rawValue = jsonControl.getRawValue();
624    if (rawValue != null)
625    {
626      return new GenerateAccessTokenResponseControl(jsonControl.getOID(),
627           jsonControl.getCriticality(), rawValue);
628    }
629
630
631    final JSONObject valueObject = jsonControl.getValueObject();
632    final ObjectTrio<String,Date,String> valueElements =
633         decodeJSONObject(valueObject);
634
635    if (strict)
636    {
637      final List<String> unrecognizedFields =
638           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
639                valueObject, JSON_FIELD_ACCESS_TOKEN,
640                JSON_FIELD_EXPIRATION_TIME, JSON_FIELD_ERROR_MESSAGE);
641      if (! unrecognizedFields.isEmpty())
642      {
643        throw new LDAPException(ResultCode.DECODING_ERROR,
644             ERR_GENERATE_TOKEN_RESPONSE_JSON_CONTROL_UNRECOGNIZED_FIELD.get(
645                  controlObject.toSingleLineString(),
646                  unrecognizedFields.get(0)));
647      }
648    }
649
650
651    return new GenerateAccessTokenResponseControl(valueElements.getFirst(),
652         valueElements.getSecond(), valueElements.getThird());
653  }
654
655
656
657  /**
658   * {@inheritDoc}
659   */
660  @Override()
661  public void toString(@NotNull final StringBuilder buffer)
662  {
663    buffer.append("GenerateAccessTokenResponseControl(hasAccessToken=");
664    buffer.append(accesToken != null);
665
666    if (expirationTime != null)
667    {
668      buffer.append(", expirationTime='");
669      buffer.append(StaticUtils.encodeRFC3339Time(expirationTime));
670      buffer.append('\'');
671    }
672
673    if (errorMessage != null)
674    {
675      buffer.append(", errorMessage='");
676      buffer.append(errorMessage);
677      buffer.append('\'');
678    }
679
680    buffer.append(')');
681  }
682}