All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ocadotechnology.sttp.oauth2.cache.future.FutureCachingAccessTokenProvider.scala Maven / Gradle / Ivy

The newest version!
package org.polyvariant.sttp.oauth2.cache.future

import cats.data.OptionT
import cats.implicits._
import org.polyvariant.sttp.oauth2.cache.ExpiringCache
import org.polyvariant.sttp.oauth2.cache.future.FutureCachingAccessTokenProvider.TokenWithExpirationTime
import org.polyvariant.sttp.oauth2.common.Scope
import org.polyvariant.sttp.oauth2.AccessTokenProvider
import org.polyvariant.sttp.oauth2.ClientCredentialsToken
import org.polyvariant.sttp.oauth2.Secret

import java.time.Instant
import scala.concurrent.duration.Duration
import monix.execution.AsyncSemaphore
import scala.concurrent.Future
import scala.concurrent.ExecutionContext

final class FutureCachingAccessTokenProvider(
  delegate: AccessTokenProvider[Future],
  tokenCache: ExpiringCache[Future, Option[Scope], TokenWithExpirationTime],
  timeProvider: TimeProvider
)(
  implicit ec: ExecutionContext
) extends AccessTokenProvider[Future] {

  val semaphore: AsyncSemaphore = AsyncSemaphore(provisioned = 1)

  override def requestToken(scope: Option[Scope]): Future[ClientCredentialsToken.AccessTokenResponse] =
    getFromCache(scope)
      .getOrElseF(semaphore.withPermit(() => acquireToken(scope))) // semaphore prevents concurrent token fetch from external service

  private def acquireToken(scope: Option[Scope]) =
    getFromCache(scope) // duplicate cache check, to verify if any other thread filled the cache during wait for semaphore permit
      .getOrElseF(fetchAndSaveToken(scope))

  private def getFromCache(scope: Option[Scope]) =
    OptionT(tokenCache.get(scope)).map(_.toAccessTokenResponse(timeProvider.currentInstant()))

  private def fetchAndSaveToken(scope: Option[Scope]) =
    for {
      token <- delegate.requestToken(scope)
      tokenWithExpiry = calculateExpiryInstant(token)
      _     <- tokenCache.put(scope, tokenWithExpiry, tokenWithExpiry.expirationTime)
    } yield token

  private def calculateExpiryInstant(response: ClientCredentialsToken.AccessTokenResponse): TokenWithExpirationTime =
    TokenWithExpirationTime.from(response, timeProvider.currentInstant())

}

object FutureCachingAccessTokenProvider {

  def apply(
    delegate: AccessTokenProvider[Future],
    tokenCache: ExpiringCache[Future, Option[Scope], TokenWithExpirationTime],
    timeProvider: TimeProvider = TimeProvider.default
  )(
    implicit ec: ExecutionContext
  ): FutureCachingAccessTokenProvider = new FutureCachingAccessTokenProvider(delegate, tokenCache, timeProvider)

  def monixCacheInstance(
    delegate: AccessTokenProvider[Future],
    timeProvider: TimeProvider = TimeProvider.default
  )(
    implicit ec: ExecutionContext
  ): FutureCachingAccessTokenProvider = FutureCachingAccessTokenProvider(delegate, MonixFutureCache(), timeProvider)

  final case class TokenWithExpirationTime(
    accessToken: Secret[String],
    domain: Option[String],
    expirationTime: Instant,
    scope: Option[Scope]
  ) {

    def toAccessTokenResponse(now: Instant): ClientCredentialsToken.AccessTokenResponse = {
      val newExpiresIn = Duration.fromNanos(java.time.Duration.between(now, expirationTime).toNanos)
      ClientCredentialsToken.AccessTokenResponse(accessToken, domain, newExpiresIn, scope)
    }

  }

  object TokenWithExpirationTime {

    def from(token: ClientCredentialsToken.AccessTokenResponse, now: Instant): TokenWithExpirationTime = {
      val expirationTime = now.plusNanos(token.expiresIn.toNanos)
      TokenWithExpirationTime(token.accessToken, token.domain, expirationTime, token.scope)
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy