content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements. See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License. You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.kafka.common.security.oauthbearer.internals.unsecured;
  
  import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
  
  import java.util.List;
  import java.util.Objects;
  import java.util.Set;
  
  public class OAuthBearerValidationUtils {
      /**
       * Validate the given claim for existence and type. It can be required to exist
       * in the given claims, and if it exists it must be one of the types indicated
       * 
       * @param jwt
       *            the mandatory JWT to which the validation will be applied
       * @param required
       *            true if the claim is required to exist
       * @param claimName
       *            the required claim name identifying the claim to be checked
       * @param allowedTypes
       *            one or more of {@code String.class}, {@code Number.class}, and
       *            {@code List.class} identifying the type(s) that the claim value is
       *            allowed to be if it exists
       * @return the result of the validation
       */
      public static OAuthBearerValidationResult validateClaimForExistenceAndType(OAuthBearerUnsecuredJws jwt,
              boolean required, String claimName, Class<?>... allowedTypes) {
          Object rawClaim = Objects.requireNonNull(jwt).rawClaim(Objects.requireNonNull(claimName));
          if (rawClaim == null)
              return required
                      ? OAuthBearerValidationResult.newFailure(String.format("Required claim missing: %s", claimName))
                      : OAuthBearerValidationResult.newSuccess();
          for (Class<?> allowedType : allowedTypes) {
              if (allowedType != null && allowedType.isAssignableFrom(rawClaim.getClass()))
                  return OAuthBearerValidationResult.newSuccess();
          }
          return OAuthBearerValidationResult.newFailure(String.format("The %s claim had the incorrect type: %s",
                  claimName, rawClaim.getClass().getSimpleName()));
      }
  
      /**
       * Validate the 'iat' (Issued At) claim. It can be required to exist in the
       * given claims, and if it exists it must be a (potentially fractional) number
       * of seconds since the epoch defining when the JWT was issued; it is a
       * validation error if the Issued At time is after the time at which the check
       * is being done (plus any allowable clock skew).
       * 
       * @param jwt
       *            the mandatory JWT to which the validation will be applied
       * @param required
       *            true if the claim is required to exist
       * @param whenCheckTimeMs
       *            the time relative to which the validation is to occur
       * @param allowableClockSkewMs
       *            non-negative number to take into account some potential clock skew
       * @return the result of the validation
       * @throws OAuthBearerConfigException
       *             if the given allowable clock skew is negative
       */
      public static OAuthBearerValidationResult validateIssuedAt(OAuthBearerUnsecuredJws jwt, boolean required,
              long whenCheckTimeMs, int allowableClockSkewMs) throws OAuthBearerConfigException {
          Number value;
          try {
              value = Objects.requireNonNull(jwt).issuedAt();
          } catch (OAuthBearerIllegalTokenException e) {
              return e.reason();
          }
          boolean exists = value != null;
          if (!exists)
              return doesNotExistResult(required, "iat");
          double doubleValue = value.doubleValue();
          return 1000 * doubleValue > whenCheckTimeMs + confirmNonNegative(allowableClockSkewMs)
                  ? OAuthBearerValidationResult.newFailure(String.format(
                          "The Issued At value (%f seconds) was after the indicated time (%d ms) plus allowable clock skew (%d ms)",
                          doubleValue, whenCheckTimeMs, allowableClockSkewMs))
                  : OAuthBearerValidationResult.newSuccess();
      }
  
      /**
       * Validate the 'exp' (Expiration Time) claim. It must exist and it must be a
       * (potentially fractional) number of seconds defining the point at which the
       * JWT expires. It is a validation error if the time at which the check is being
       * done (minus any allowable clock skew) is on or after the Expiration Time
       * time.
       * 
       * @param jwt
       *            the mandatory JWT to which the validation will be applied
       * @param whenCheckTimeMs
       *            the time relative to which the validation is to occur
       * @param allowableClockSkewMs
       *            non-negative number to take into account some potential clock skew
       * @return the result of the validation
       * @throws OAuthBearerConfigException
       *             if the given allowable clock skew is negative
       */
      public static OAuthBearerValidationResult validateExpirationTime(OAuthBearerUnsecuredJws jwt, long whenCheckTimeMs,
              int allowableClockSkewMs) throws OAuthBearerConfigException {
          Number value;
          try {
              value = Objects.requireNonNull(jwt).expirationTime();
          } catch (OAuthBearerIllegalTokenException e) {
              return e.reason();
          }
          boolean exists = value != null;
          if (!exists)
              return doesNotExistResult(true, "exp");
          double doubleValue = value.doubleValue();
          return whenCheckTimeMs - confirmNonNegative(allowableClockSkewMs) >= 1000 * doubleValue
                  ? OAuthBearerValidationResult.newFailure(String.format(
                          "The indicated time (%d ms) minus allowable clock skew (%d ms) was on or after the Expiration Time value (%f seconds)",
                          whenCheckTimeMs, allowableClockSkewMs, doubleValue))
                  : OAuthBearerValidationResult.newSuccess();
      }
  
      /**
       * Validate the 'iat' (Issued At) and 'exp' (Expiration Time) claims for
       * internal consistency. The following must be true if both claims exist:
       * 
       * <pre>
       * exp > iat
       * </pre>
       * 
       * @param jwt
       *            the mandatory JWT to which the validation will be applied
       * @return the result of the validation
       */
      public static OAuthBearerValidationResult validateTimeConsistency(OAuthBearerUnsecuredJws jwt) {
          Number issuedAt;
          Number expirationTime;
          try {
              issuedAt = Objects.requireNonNull(jwt).issuedAt();
              expirationTime = jwt.expirationTime();
          } catch (OAuthBearerIllegalTokenException e) {
              return e.reason();
          }
          if (expirationTime != null && issuedAt != null && expirationTime.doubleValue() <= issuedAt.doubleValue())
              return OAuthBearerValidationResult.newFailure(
                      String.format("The Expiration Time time (%f seconds) was not after the Issued At time (%f seconds)",
                              expirationTime.doubleValue(), issuedAt.doubleValue()));
          return OAuthBearerValidationResult.newSuccess();
      }
  
      /**
       * Validate the given token's scope against the required scope. Every required
       * scope element (if any) must exist in the provided token's scope for the
       * validation to succeed.
       * 
       * @param token
       *            the required token for which the scope will to validate
       * @param requiredScope
       *            the optional required scope against which the given token's scope
       *            will be validated
       * @return the result of the validation
       */
      public static OAuthBearerValidationResult validateScope(OAuthBearerToken token, List<String> requiredScope) {
          final Set<String> tokenScope = token.scope();
          if (requiredScope == null || requiredScope.isEmpty())
              return OAuthBearerValidationResult.newSuccess();
          for (String requiredScopeElement : requiredScope) {
              if (!tokenScope.contains(requiredScopeElement))
                  return OAuthBearerValidationResult.newFailure(String.format(
                          "The provided scope (%s) was missing a required scope (%s).  All required scope elements: %s",
                          tokenScope, requiredScopeElement, requiredScope),
                          requiredScope.toString(), null);
          }
          return OAuthBearerValidationResult.newSuccess();
      }
  
      private static int confirmNonNegative(int allowableClockSkewMs) throws OAuthBearerConfigException {
          if (allowableClockSkewMs < 0)
              throw new OAuthBearerConfigException(
                      String.format("Allowable clock skew must not be negative: %d", allowableClockSkewMs));
          return allowableClockSkewMs;
      }
  
      private static OAuthBearerValidationResult doesNotExistResult(boolean required, String claimName) {
          return required ? OAuthBearerValidationResult.newFailure(String.format("Required claim missing: %s", claimName))
                  : OAuthBearerValidationResult.newSuccess();
      }
  
      private OAuthBearerValidationUtils() {
          // empty
      }
  }
  