001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-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) 2020-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.Collections;
041import java.util.Iterator;
042import java.util.LinkedHashSet;
043import java.util.Set;
044import java.util.StringTokenizer;
045
046import com.unboundid.asn1.ASN1OctetString;
047import com.unboundid.util.Debug;
048import com.unboundid.util.NotMutable;
049import com.unboundid.util.NotNull;
050import com.unboundid.util.Nullable;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.json.JSONObject;
054
055
056
057/**
058 * This class provides a bind result that can provide access to the details of
059 * a failed OAUTHBEARER SASL bind attempt.
060 *
061 * @see  OAUTHBEARERBindRequest
062 */
063@NotMutable()
064@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
065public final class OAUTHBEARERBindResult
066       extends BindResult
067{
068  /**
069   * The name of the failure details field that holds the authorization error
070   * code.
071   */
072  @NotNull private static final String FAILURE_DETAILS_FIELD_AUTHZ_ERROR_CODE =
073       "status";
074
075
076
077  /**
078   * The name of the failure details field that holds the OpenID configuration
079   * URL.
080   */
081  @NotNull private static final String FAILURE_DETAILS_FIELD_OPENID_CONFIG_URL =
082       "openid-configuration";
083
084
085
086  /**
087   * The name of the failure details field that holds the space-delimited set of
088   * scopes.
089   */
090  @NotNull private static final String FAILURE_DETAILS_FIELD_SCOPE = "scope";
091
092
093
094  /**
095   * The serial version UID for this serializable class.
096   */
097  private static final long serialVersionUID = 6513765034667496311L;
098
099
100
101  // The final bind result received during bind processing.
102  @Nullable private final BindResult finalBindResult;
103
104  // The initial bind result received during bind processing.
105  @NotNull private final BindResult initialBindResult;
106
107  // A JSON object with additional details about a failed authentication
108  // attempt.
109  @Nullable private final JSONObject failureDetailsObject;
110
111  // The set of scopes included in the failure details object.
112  @NotNull private final Set<String> scopes;
113
114  // The authorization error code included in the failure details object.
115  @Nullable private final String authorizationErrorCode;
116
117  // The OpenID configuration URL included in the failure details object.
118  @Nullable private final String openIDConfigurationURL;
119
120
121
122  /**
123   * Creates a new OAUTHBEARER bind result from the provided single bind
124   * result.  The provided result is not expected to contain server SASL
125   * credentials, but it will attempt to decode any credentials included in the
126   * provided result.
127   *
128   * @param  bindResult  The bind result to use to create this OAUTHBEARER bind
129   *                     result.  It must not be {@code null}.
130   */
131  public OAUTHBEARERBindResult(@NotNull final BindResult bindResult)
132  {
133    this(bindResult, null);
134  }
135
136
137
138  /**
139   * Creates a new OAUTHBEARER bind result from the provided pair of results,
140   * which correspond to the initial and final (if any) phases of bind
141   * processing.
142   *
143   * @param  initialBindResult  The result obtained in response to the initial
144   *                            OAUTHBEARER bind request.  It must not be
145   *                            {@code null}.
146   * @param  finalBindResult    The result obtained in response to the final
147   *                            OAUTHBEARER bind request, if any.  It may be
148   *                            {@code null} if the bind consisted of only a
149   *                            single request.
150   */
151  public OAUTHBEARERBindResult(@NotNull final BindResult initialBindResult,
152                               @Nullable final BindResult finalBindResult)
153  {
154    super(mergeBindResults(initialBindResult, finalBindResult));
155
156    this.initialBindResult = initialBindResult;
157    this.finalBindResult = finalBindResult;
158
159    final ASN1OctetString serverSASLCredentials =
160         initialBindResult.getServerSASLCredentials();
161    if (serverSASLCredentials == null)
162    {
163      failureDetailsObject = null;
164      authorizationErrorCode = null;
165      scopes = Collections.emptySet();
166      openIDConfigurationURL = null;
167      return;
168    }
169
170    final JSONObject credentialsObject;
171    try
172    {
173      credentialsObject = new JSONObject(serverSASLCredentials.stringValue());
174    }
175    catch (final Exception e)
176    {
177      Debug.debugException(e);
178      failureDetailsObject = null;
179      authorizationErrorCode = null;
180      scopes = Collections.emptySet();
181      openIDConfigurationURL = null;
182      return;
183    }
184
185    failureDetailsObject = credentialsObject;
186    authorizationErrorCode = credentialsObject.getFieldAsString(
187         FAILURE_DETAILS_FIELD_AUTHZ_ERROR_CODE);
188    openIDConfigurationURL = credentialsObject.getFieldAsString(
189         FAILURE_DETAILS_FIELD_OPENID_CONFIG_URL);
190
191    final String scopeStr =
192         credentialsObject.getFieldAsString(FAILURE_DETAILS_FIELD_SCOPE);
193    if (scopeStr == null)
194    {
195      scopes = Collections.emptySet();
196      return;
197    }
198
199    final Set<String> scopeSet = new LinkedHashSet<>();
200    final StringTokenizer tokenizer = new StringTokenizer(scopeStr, " ");
201    while (tokenizer.hasMoreTokens())
202    {
203      scopeSet.add(tokenizer.nextToken());
204    }
205    scopes = Collections.unmodifiableSet(scopeSet);
206  }
207
208
209
210  /**
211   * Creates a bind result that is merged from the provided results.  If the
212   * provided final result is {@code null}, then this will simply return the
213   * initial result.  If both are non-{@code null}, then it will use all details
214   * from the final result except the server SASL credentials, which will come
215   * from the initial result.
216   *
217   * @param  initialBindResult  The result obtained in response to the initial
218   *                            OAUTHBEARER bind request.  It must not be
219   *                            {@code null}.
220   * @param  finalBindResult    The result obtained in response to the final
221   *                            OAUTHBEARER bind request, if any.  It may be
222   *                            {@code null} if the bind consisted of only a
223   *                            single request.
224   *
225   * @return  The merged bind results.
226   */
227  @NotNull()
228  private static BindResult mergeBindResults(
229               @NotNull final BindResult initialBindResult,
230               @Nullable final BindResult finalBindResult)
231  {
232    if (finalBindResult == null)
233    {
234      return initialBindResult;
235    }
236
237    return new BindResult(finalBindResult.getMessageID(),
238         finalBindResult.getResultCode(),
239         finalBindResult.getDiagnosticMessage(),
240         finalBindResult.getMatchedDN(),
241         finalBindResult.getReferralURLs(),
242         finalBindResult.getResponseControls(),
243         initialBindResult.getServerSASLCredentials());
244  }
245
246
247
248  /**
249   * Retrieves the result obtained from the initial bind attempt in the
250   * OAUTHBEARER authentication process.  For a successful authentication, there
251   * should only be a single bind.  For a failed authentication attempt, there
252   * may be either one or two binds, based on whether credentials were included
253   * in the initial bind result.
254   *
255   * @return  The result obtained from the initial bind attempt in the
256   *          OAUTHBEARER authentication process.
257   */
258  @NotNull()
259  public BindResult getInitialBindResult()
260  {
261    return initialBindResult;
262  }
263
264
265
266  /**
267   * Retrieves the result obtained from the final bind attempt in the
268   * OAUTHBEARER authentication process, if any.  This should always be
269   * {@code null} for a successful bind, and it may or may not be {@code null}
270   * for a failed attempt, based on whether credentials were included in the
271   * initial bind result.
272   *
273   * @return  The result obtained for the final bind attempt in the
274   *          OAUTHBEARER authentication process, or {@code null} if the
275   *          authentication process only included a single bind.
276   */
277  @Nullable()
278  public BindResult getFinalBindResult()
279  {
280    return finalBindResult;
281  }
282
283
284
285  /**
286   * Retrieves a JSON object with additional information about a failed
287   * authentication attempt, if any.
288   *
289   * @return  A JSON object with additional information about a failed
290   *          authentication attempt, or {@code null} this is not available.
291   */
292  @Nullable()
293  public JSONObject getFailureDetailsObject()
294  {
295    return failureDetailsObject;
296  }
297
298
299
300  /**
301   * Retrieves the authorization error code obtained from the failure details
302   * object, if available.
303   *
304   * @return  The authorization error code obtained from the failure details
305   *          object, or {@code null} if no failure details object was provided
306   *          or if it did not include an authorization error code.
307   */
308  @Nullable()
309  public String getAuthorizationErrorCode()
310  {
311    return authorizationErrorCode;
312  }
313
314
315
316  /**
317   * Retrieves the set of scopes included in the failure details object, if
318   * available.
319   *
320   * @return  The set of scopes included in teh failure details object, or an
321   *          empty set if no failure details object was provided or if it did
322   *          not include any scopes.
323   */
324  @NotNull()
325  public Set<String> getScopes()
326  {
327    return scopes;
328  }
329
330
331
332  /**
333   * Retrieves the OpenID configuration URL obtained from the failure details
334   * object, if available.
335   *
336   * @return  The OpenID configuration URL obtained from the failure details
337   *          object, or {@code null} if no failure details object was provided
338   *          or if it did not include an OpenID configuration URL.
339   */
340  @Nullable()
341  public String getOpenIDConfigurationURL()
342  {
343    return openIDConfigurationURL;
344  }
345
346
347
348  /**
349   * {@inheritDoc}
350   */
351  @Override()
352  public void toString(@NotNull final StringBuilder buffer)
353  {
354    buffer.append("OAUTHBEARERBindResult(resultCode=");
355    buffer.append(getResultCode());
356
357    final int messageID = getMessageID();
358    if (messageID >= 0)
359    {
360      buffer.append(", messageID=");
361      buffer.append(messageID);
362    }
363
364    final String diagnosticMessage = getDiagnosticMessage();
365    if (diagnosticMessage != null)
366    {
367      buffer.append(", diagnosticMessage='");
368      buffer.append(diagnosticMessage);
369      buffer.append('\'');
370    }
371
372    final String matchedDN = getMatchedDN();
373    if (matchedDN != null)
374    {
375      buffer.append(", matchedDN='");
376      buffer.append(matchedDN);
377      buffer.append('\'');
378    }
379
380    final String[] referralURLs = getReferralURLs();
381    if (referralURLs.length > 0)
382    {
383      buffer.append(", referralURLs={");
384      for (int i=0; i < referralURLs.length; i++)
385      {
386        if (i > 0)
387        {
388          buffer.append(", ");
389        }
390
391        buffer.append('\'');
392        buffer.append(referralURLs[i]);
393        buffer.append('\'');
394      }
395      buffer.append('}');
396    }
397
398    buffer.append(", hasServerSASLCredentials=");
399    buffer.append(getServerSASLCredentials() != null);
400
401    if (finalBindResult != null)
402    {
403      buffer.append(", initialBindResult=");
404      initialBindResult.toString(buffer);
405
406      buffer.append(", finalBindResult=");
407      finalBindResult.toString(buffer);
408    }
409
410    if (failureDetailsObject != null)
411    {
412      buffer.append(", failureDetailsObject=");
413      failureDetailsObject.toSingleLineString(buffer);
414    }
415
416    if (authorizationErrorCode != null)
417    {
418      buffer.append(", authorizationErrorCode='");
419      buffer.append(authorizationErrorCode);
420      buffer.append('\'');
421    }
422
423    if (! scopes.isEmpty())
424    {
425      buffer.append(", scopes={");
426
427      final Iterator<String> iterator = scopes.iterator();
428      while (iterator.hasNext())
429      {
430        buffer.append(' ');
431        buffer.append(iterator.next());
432
433        if (iterator.hasNext())
434        {
435          buffer.append(',');
436        }
437      }
438
439      buffer.append(" }");
440    }
441
442    if (openIDConfigurationURL != null)
443    {
444      buffer.append(", openIDConfigURL='");
445      buffer.append(openIDConfigurationURL);
446      buffer.append('\'');
447    }
448
449    final Control[] responseControls = getResponseControls();
450    if (responseControls.length > 0)
451    {
452      buffer.append(", responseControls={");
453      for (int i=0; i < responseControls.length; i++)
454      {
455        if (i > 0)
456        {
457          buffer.append(", ");
458        }
459
460        buffer.append(responseControls[i]);
461      }
462      buffer.append('}');
463    }
464
465    buffer.append(')');
466  }
467}