com.cognite.sdk.scala.common.OAuth2.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cognite-sdk-scala_2.13 Show documentation
Show all versions of cognite-sdk-scala_2.13 Show documentation
Scala SDK for Cognite Data Fusion.
The newest version!
package com.cognite.sdk.scala.common
import cats.Monad
import cats.syntax.all._
import cats.effect.{Async, Clock}
import com.cognite.sdk.scala.common.internal.{CachedResource, ConcurrentCachedObject}
import com.cognite.sdk.scala.v1.GenericClient.parseResponse
import com.cognite.sdk.scala.v1.{RefreshSessionRequest, SessionTokenResponse}
import com.cognite.sdk.scala.v1.resources.Sessions.{
refreshSessionRequestEncoder,
sessionTokenDecoder
}
import fs2.io.file.{Files, Path}
import sttp.client3._
import sttp.client3.circe.asJson
import io.circe.Decoder
import io.circe.generic.semiauto.deriveDecoder
import sttp.model.Uri
object OAuth2 {
final case class ClientCredentials(
tokenUri: Uri,
clientId: String,
clientSecret: String,
scopes: List[String] = List.empty,
cdfProjectName: String,
audience: Option[String] = None
) {
def getAuth[F[_]](
implicit F: Async[F],
clock: Clock[F],
sttpBackend: SttpBackend[F, Any]
): F[TokenState] = {
val body = Map[String, String](
"grant_type" -> "client_credentials",
"client_id" -> clientId,
"client_secret" -> clientSecret,
// Aize auth flow requires this to be empty in general
"scope" -> scopes.mkString(" "), // Send empty scopes when it is not provided
// AAD auth flow requires this to be empty
"audience" -> audience.getOrElse(
""
) // Send empty audience when it is not provided
)
for {
acquiredLowerBound <- clock.realTime.map(_.toSeconds)
response <- basicRequest
.header("Accept", "application/json")
.post(tokenUri)
.body(body)
.response(asJson[ClientCredentialsResponse])
.send(sttpBackend)
payload <- response.body match {
case Right(response) => F.pure(response)
case Left(responseException) => // Non-2xx response
responseException match {
case HttpError(body, statusCode) =>
F.raiseError[ClientCredentialsResponse](
new SdkException(
body,
uri = Some(tokenUri),
responseCode = Some(statusCode.code)
)
)
case DeserializationException(_, error) =>
F.raiseError(
new SdkException(
s"Failed to parse response from IdP: ${error.getMessage}"
)
)
}
}
expiresAt = acquiredLowerBound + payload.expires_in
} yield TokenState(payload.access_token, expiresAt)
}
}
final case class Session(
baseUrl: String,
sessionId: Long,
sessionKey: String,
cdfProjectName: String
) {
/** Only use for SessionProvider to interact with Cognite internal SessionAPI */
private def getKubernetesJwt[F[_]](implicit F: Async[F]): F[String] = {
val serviceAccountTokenPath = Path("/var/run/secrets/tokens/cdf_token")
Files
.forAsync[F]
.readAll(serviceAccountTokenPath)
.through(fs2.text.utf8.decode)
.compile
.string
.adaptError { case err =>
new SdkException(s"Failed to get service token because ${err.getMessage}")
}
}
def getAuth[F[_]](
getToken: Option[F[String]] = None
)(
implicit F: Async[F],
clock: Clock[F],
sttpBackend: SttpBackend[F, Any]
): F[TokenState] = {
import sttp.client3.circe._
val uri = uri"${baseUrl}/api/v1/projects/${cdfProjectName}/sessions/token"
for {
kubernetesServiceToken <- getToken.getOrElse(getKubernetesJwt)
acquiredLowerBound <- clock.realTime.map(_.toSeconds)
payload <- basicRequest
.header("Accept", "application/json")
.header("Authorization", s"Bearer ${kubernetesServiceToken}")
.post(uri)
.body(RefreshSessionRequest(sessionId, sessionKey))
.response(
parseResponse[SessionTokenResponse, SessionTokenResponse](uri, value => value)
)
.send(sttpBackend)
.flatMap(r => F.fromEither(r.body))
expiresAt = acquiredLowerBound + payload.expiresIn
} yield TokenState(payload.accessToken, expiresAt)
}
}
private def commonGetAuth[F[_]](
cache: CachedResource[F, TokenState],
refreshSecondsBeforeExpiration: Long
)(
implicit F: Monad[F],
clock: Clock[F]
): F[Auth] =
for {
now <- clock.realTime.map(_.toSeconds)
_ <- cache.invalidateIfNeeded(_.expiresAt - refreshSecondsBeforeExpiration <= now)
auth <- cache.run(state => F.pure(OidcTokenAuth(state.token)))
} yield auth
class ClientCredentialsProvider[F[_]] private (
val cache: CachedResource[F, TokenState],
val refreshSecondsBeforeExpiration: Long
)(
implicit F: Monad[F],
clock: Clock[F]
) extends AuthProvider[F]
with Serializable {
def getAuth: F[Auth] = commonGetAuth(cache, refreshSecondsBeforeExpiration)
}
class SessionProvider[F[_]] private (
val cache: CachedResource[F, TokenState],
val refreshSecondsBeforeExpiration: Long
)(implicit F: Monad[F], clock: Clock[F])
extends AuthProvider[F]
with Serializable {
def getAuth: F[Auth] = commonGetAuth(cache, refreshSecondsBeforeExpiration)
}
object ClientCredentialsProvider {
def apply[F[_]](
credentials: ClientCredentials,
refreshSecondsBeforeExpiration: Long = 30,
initialToken: Option[TokenState] = None
)(
implicit F: Async[F],
sttpBackend: SttpBackend[F, Any]
): F[ClientCredentialsProvider[F]] =
ConcurrentCachedObject(credentials.getAuth, initialToken)
.map(new ClientCredentialsProvider[F](_, refreshSecondsBeforeExpiration))
}
object SessionProvider {
def apply[F[_]](
session: Session,
refreshSecondsBeforeExpiration: Long = 30,
getToken: Option[F[String]] = None,
initialToken: Option[TokenState] = None
)(
implicit F: Async[F],
sttpBackend: SttpBackend[F, Any]
): F[SessionProvider[F]] =
ConcurrentCachedObject(session.getAuth(getToken = getToken), initialToken)
.map(new SessionProvider[F](_, refreshSecondsBeforeExpiration))
}
private final case class ClientCredentialsResponse(access_token: String, expires_in: Long)
private object ClientCredentialsResponse {
implicit val decoder: Decoder[ClientCredentialsResponse] = deriveDecoder
}
final case class TokenState(token: String, expiresAt: Long)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy