AccessTokenCache.java

/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed 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.bremersee.security.authentication;

import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
import java.text.ParseException;
import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import javax.validation.constraints.NotNull;
import org.bremersee.exception.ServiceException;
import org.springframework.cache.Cache;
import org.springframework.validation.annotation.Validated;

/**
 * The access token cache interface.
 *
 * @author Christian Bremer
 */
@Validated
public interface AccessTokenCache {

  /**
   * The constant CACHE_NAME.
   */
  String CACHE_NAME = "jwt";

  /**
   * Find not expired access token from cache.
   *
   * @param key the key
   * @return the access token
   */
  Optional<String> findAccessToken(@NotNull String key);

  /**
   * Put new access token into the cache.
   *
   * @param key the key
   * @param accessToken the access token
   */
  void putAccessToken(@NotNull String key, @NotNull String accessToken);

  /**
   * Checks whether the access token is expired. If no expiration claim is present, the result will always be {@code
   * true}.
   *
   * @param tokenValue the token value
   * @param accessTokenThreshold the access token threshold
   * @return the boolean
   */
  static boolean isExpired(@NotNull String tokenValue, Duration accessTokenThreshold) {
    Duration duration = Objects
        .requireNonNullElseGet(accessTokenThreshold, () -> Duration.ofSeconds(20L));
    long millis = System.currentTimeMillis() + duration.toMillis();
    return Optional.ofNullable(getExpirationTime(tokenValue))
        .map(date -> date.getTime() < millis)
        .orElse(true);
  }

  /**
   * Gets expiration time.
   *
   * @param tokenValue the token value
   * @return the expiration time or {@code null} if there is no expiration claim
   */
  static Date getExpirationTime(@NotNull String tokenValue) {
    JWT jwt = parse(tokenValue);
    try {
      if (jwt.getJWTClaimsSet() != null
          && jwt.getJWTClaimsSet().getExpirationTime() != null) {
        return jwt.getJWTClaimsSet().getExpirationTime();
      }

    } catch (ParseException e) {
      // ignored
    }
    return null;
  }

  /**
   * Parse jwt.
   *
   * @param tokenValue the token value
   * @return the jwt
   */
  static JWT parse(@NotNull String tokenValue) {
    try {
      return SignedJWT.parse(tokenValue);
    } catch (Exception e0) {
      try {
        return EncryptedJWT.parse(tokenValue);
      } catch (Exception e1) {
        try {
          return PlainJWT.parse(tokenValue);
        } catch (Exception e2) {
          throw ServiceException.internalServerError("Parsing jwt failed.");
        }
      }
    }
  }

  /**
   * Creates a new builder.
   *
   * @return the builder
   */
  static Builder builder() {
    return new Builder.Impl();
  }

  /**
   * The builder interface.
   */
  interface Builder {

    /**
     * With external cache.
     *
     * @param externalCache the external cache
     * @return the builder
     */
    Builder withExternalCache(Cache externalCache);

    /**
     * With expiration time threshold.
     *
     * @param duration the duration
     * @return the builder
     */
    Builder withExpirationTimeThreshold(Duration duration);

    /**
     * With key prefix.
     *
     * @param keyPrefix the key prefix
     * @return the builder
     */
    Builder withKeyPrefix(String keyPrefix);

    /**
     * Build access token cache.
     *
     * @return the access token cache
     */
    AccessTokenCache build();

    /**
     * The builder implementation.
     */
    class Impl implements Builder {

      private Cache externalCache;

      private Duration expirationTimeThreshold;

      private String keyPrefix;

      @Override
      public Builder withExternalCache(Cache externalCache) {
        this.externalCache = externalCache;
        return this;
      }

      @Override
      public Builder withExpirationTimeThreshold(Duration duration) {
        this.expirationTimeThreshold = duration;
        return this;
      }

      @Override
      public Builder withKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
        return this;
      }

      @Override
      public AccessTokenCache build() {
        return new AccessTokenCacheImpl(externalCache, expirationTimeThreshold, keyPrefix);
      }
    }
  }
}