Skip to content

Commit

Permalink
Merge pull request #46 from bwiercinski/client-credentials-backend
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Mar 11, 2021
2 parents 1afbd16 + 5c5a8b0 commit a404b5c
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- run: sbt ++${{ matrix.scala }} test mimaReportBinaryIssues

- name: Compress target directories
run: tar cf targets.tar target oauth2/target project/target
run: tar cf targets.tar target oauth2/target oauth2-backend-cats/target project/target

- name: Upload target directories
uses: actions/upload-artifact@v2
Expand Down
67 changes: 30 additions & 37 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,64 +57,57 @@ ThisBuild / githubWorkflowEnv ++= List("PGP_PASSPHRASE", "PGP_SECRET", "SONATYPE

val Versions = new {
val catsCore = "2.4.2"
val catsEffect = "2.3.1"
val circe = "0.13.0"
val kindProjector = "0.11.3"
val scalaTest = "3.2.6"
val sttp = "3.1.7"
val refined = "0.9.21"
}

val commonDependencies = {

val cats = Seq(
"org.typelevel" %% "cats-core" % Versions.catsCore
)

val circe = Seq(
"io.circe" %% "circe-parser" % Versions.circe,
"io.circe" %% "circe-core" % Versions.circe,
"io.circe" %% "circe-refined" % Versions.circe
)

val plugins = Seq(
compilerPlugin("org.typelevel" % "kind-projector" % Versions.kindProjector cross CrossVersion.full)
)

val sttp = Seq(
"com.softwaremill.sttp.client3" %% "core" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "circe" % Versions.sttp
)

val refined = Seq(
"eu.timepit" %% "refined" % Versions.refined
)

cats ++ circe ++ sttp ++ refined ++ plugins
}

val oauth2Dependencies = {
val testDependencies = Seq(
"org.scalatest" %% "scalatest" % Versions.scalaTest,
"io.circe" %% "circe-literal" % Versions.circe
).map(_ % Test)
val plugins = Seq(
compilerPlugin("org.typelevel" % "kind-projector" % Versions.kindProjector cross CrossVersion.full),
compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
)

commonDependencies ++ testDependencies
}
val testDependencies = Seq(
"org.scalatest" %% "scalatest" % Versions.scalaTest,
"io.circe" %% "circe-literal" % Versions.circe
).map(_ % Test)

val mimaSettings = mimaPreviousArtifacts := Set(
// organization.value %% name.value % "0.3.0" // TODO Define a process for resetting this after release
)

lazy val oauth2 = project.settings(
name := "sttp-oauth2",
libraryDependencies ++= oauth2Dependencies,
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % Versions.catsCore,
"io.circe" %% "circe-parser" % Versions.circe,
"io.circe" %% "circe-core" % Versions.circe,
"io.circe" %% "circe-refined" % Versions.circe,
"com.softwaremill.sttp.client3" %% "core" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "circe" % Versions.sttp,
"eu.timepit" %% "refined" % Versions.refined
) ++ plugins ++ testDependencies,
mimaSettings
)

lazy val `oauth2-backend-cats` = project
.settings(
name := "sttp-oauth2-backend-cats",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % Versions.catsEffect,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.sttp % Test
) ++ plugins ++ testDependencies,
mimaSettings
)
.dependsOn(oauth2)

val root = project
.in(file("."))
.settings(
skip in publish := true,
mimaPreviousArtifacts := Set.empty
)
.aggregate(oauth2)
.aggregate(oauth2, `oauth2-backend-cats`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.ocadotechnology.sttp.oauth2.backend

import cats.effect.Sync
import cats.implicits._
import cats.effect.concurrent.Ref

trait Cache[F[_], A] {
def get: F[Option[A]]
def set(a: A): F[Unit]
}

object Cache {

def refCache[F[_]: Sync, A]: F[Cache[F, A]] = Ref[F].of(Option.empty[A]).map { ref =>
new Cache[F, A] {
override def get: F[Option[A]] = ref.get
override def set(a: A): F[Unit] = ref.set(Some(a))
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.ocadotechnology.sttp.oauth2.backend

import cats.Monad
import cats.data.OptionT
import cats.effect.Clock
import cats.effect.Concurrent
import cats.effect.concurrent.Semaphore
import cats.implicits._
import com.ocadotechnology.sttp.oauth2.ClientCredentialsProvider
import com.ocadotechnology.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
import com.ocadotechnology.sttp.oauth2.Secret
import com.ocadotechnology.sttp.oauth2.backend.SttpOauth2ClientCredentialsCatsBackend.TokenWithExpiryInstant
import com.ocadotechnology.sttp.oauth2.common.Scope
import eu.timepit.refined.types.string.NonEmptyString
import sttp.capabilities.Effect
import sttp.client3._
import sttp.model.Uri

import java.time.Instant

final class SttpOauth2ClientCredentialsCatsBackend[F[_]: Monad: Clock, P] private (
delegate: SttpBackend[F, P],
fetchTokenAction: F[AccessTokenResponse],
cache: Cache[F, TokenWithExpiryInstant],
semaphore: Semaphore[F]
) extends DelegateSttpBackend(delegate) {

override def send[T, R >: P with Effect[F]](request: Request[T, R]): F[Response[T]] = for {
token <- semaphore.withPermit(resolveToken)
response <- delegate.send(request.auth.bearer(token.value))
} yield response

private val resolveToken: F[Secret[String]] =
OptionT(cache.get)
.product(OptionT.liftF(Clock[F].instantNow))
.filter { case (TokenWithExpiryInstant(_, expiryInstant), currentInstant) => currentInstant isBefore expiryInstant }
.map(_._1)
.getOrElseF(fetchAndSaveToken)
.map(_.token)

private def fetchAndSaveToken: F[TokenWithExpiryInstant] =
fetchTokenAction.flatMap(calculateExpiryInstant).flatTap(cache.set)

private def calculateExpiryInstant(response: AccessTokenResponse): F[TokenWithExpiryInstant] =
Clock[F].instantNow.map(_ plusMillis response.expiresIn.toMillis).map(TokenWithExpiryInstant(response.accessToken, _))

}

object SttpOauth2ClientCredentialsCatsBackend {
final case class TokenWithExpiryInstant(token: Secret[String], expiryInstant: Instant)

def apply[F[_]: Concurrent: Clock, P](
tokenUrl: Uri,
tokenIntrospectionUrl: Uri,
clientId: NonEmptyString,
clientSecret: Secret[String]
)(
scope: Scope
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] = {
val clientCredentialsProvider = ClientCredentialsProvider.instance(tokenUrl, tokenIntrospectionUrl, clientId, clientSecret)
usingClientCredentialsProvider(clientCredentialsProvider)(scope)
}

/** Keep in mind that the given implicit `backend` may be different than this one used by `clientCredentialsProvider`
*/
def usingClientCredentialsProvider[F[_]: Concurrent: Clock, P](
clientCredentialsProvider: ClientCredentialsProvider[F]
)(
scope: Scope
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] =
Cache.refCache[F, TokenWithExpiryInstant].flatMap(usingClientCredentialsProviderAndCache(clientCredentialsProvider, _)(scope))

def usingCache[F[_]: Concurrent: Clock, P](
cache: Cache[F, TokenWithExpiryInstant]
)(
tokenUrl: Uri,
tokenIntrospectionUrl: Uri,
clientId: NonEmptyString,
clientSecret: Secret[String]
)(
scope: Scope
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] = {
val clientCredentialsProvider = ClientCredentialsProvider.instance(tokenUrl, tokenIntrospectionUrl, clientId, clientSecret)
usingClientCredentialsProviderAndCache(clientCredentialsProvider, cache)(scope)
}

/** Keep in mind that the given implicit `backend` may be different than this one used by `clientCredentialsProvider`
*/
def usingClientCredentialsProviderAndCache[F[_]: Concurrent: Clock, P](
clientCredentialsProvider: ClientCredentialsProvider[F],
cache: Cache[F, TokenWithExpiryInstant]
)(
scope: Scope
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] =
usingFetchTokenActionAndCache(clientCredentialsProvider.requestToken(scope), cache)

/** Keep in mind that the given implicit `backend` may be different than this one used by `fetchTokenAction`
*/
def usingFetchTokenActionAndCache[F[_]: Concurrent: Clock, P](
fetchTokenAction: F[AccessTokenResponse],
cache: Cache[F, TokenWithExpiryInstant]
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] =
Semaphore(n = 1).map(new SttpOauth2ClientCredentialsCatsBackend(backend, fetchTokenAction, cache, _))

}
Loading

0 comments on commit a404b5c

Please sign in to comment.