Skip to content

Commit

Permalink
Merge pull request #1 from permutive-engineering/feature/initial-modules
Browse files Browse the repository at this point in the history
Initial modules
  • Loading branch information
alejandrohdezma authored Jan 9, 2024
2 parents 041ff0b + 7fbafbb commit 22902f7
Show file tree
Hide file tree
Showing 30 changed files with 1,735 additions and 1 deletion.
213 changes: 212 additions & 1 deletion .github/docs/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,226 @@
@DESCRIPTION@

```scala mdoc:toc
```

## Installation

Add the following line to your build.sbt file:
Add the following line to your `build.sbt` file:

```sbt
libraryDependencies += "@ORGANIZATION@" %% "@NAME@" % "@VERSION@"
```

## Usage

This library provides a class `TokenProvider` that is able to retrieve a
specific type of access token from [Google OAuth 2.0] API.

### Available token providers

```scala mdoc:invisible
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey

import scala.concurrent.duration._

import cats.effect.IO
import cats.effect.Resource

import fs2.io.file.Path
import org.http4s.client.Client
import org.http4s.syntax.all._
import org.http4s.Response
import retry.RetryPolicies._

val httpClient: Client[IO] = Client[IO] { _ => Resource.pure(Response[IO]())}
val pathToServiceAccountFile = Path("")
val pathToClientSecretsPath = Path("")
val pathToRefreshTokenPath = Path("")
val privateKey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate().asInstanceOf[RSAPrivateKey]
```

#### Identity

Retrieves an [Identity Token] using Google's metadata server for a specific audience.

Identity tokens can be used for calling Cloud Run services.

**Important!** This method can only be run from within a workload container in
GCP. The call will fail otherwise.

```scala mdoc:silent
import com.permutive.gcp.auth.TokenProvider

val audience = uri"https://my-run-app.a.run.app"

TokenProvider.identity[IO](httpClient, audience)
```

#### Service-Account

Retrieves a [Google Service Account Token] either via the
[instance metadata API] (if running from a GCP workload) or using a
specific service account file.

```scala mdoc:silent
import com.permutive.gcp.auth.TokenProvider
import com.permutive.gcp.auth.models.ClientEmail

// Retrieves a workload service account token using
// Google's metadata server.
TokenProvider.serviceAccount[IO](httpClient)

// Retrieves a service account token using a specific
// file and scopes
TokenProvider.serviceAccount[IO](
pathToServiceAccountFile,
scope = "https://www.googleapis.com/auth/bigquery" :: Nil,
httpClient
)

// Retrieves a service account token using a specific
// email/key/scopes
TokenProvider.serviceAccount[IO](
ClientEmail("[email protected]"),
privateKey: RSAPrivateKey,
scope = "https://www.googleapis.com/auth/bigquery" :: Nil,
httpClient
)
```

#### User-Account

Retrieves a [Google User Account Token] either using the application default
credentials or from a specific path.

```scala mdoc:silent
import com.permutive.gcp.auth.TokenProvider
import com.permutive.gcp.auth.models.ClientId
import com.permutive.gcp.auth.models.ClientSecret
import com.permutive.gcp.auth.models.RefreshToken

// Retrieves a user account token using a specific file
// for the secrets and token
TokenProvider.userAccount[IO](
pathToClientSecretsPath,
pathToRefreshTokenPath,
httpClient
)

// Retrieves a service account token using a specific
// client-id/client-secret/refresh-token
TokenProvider.userAccount[IO](
ClientId("client-id"),
ClientSecret("client-secret"),
RefreshToken("refresh-token"),
httpClient
)

// Retrieves a user account token using the application
// default credentials
TokenProvider.userAccount[IO](httpClient)
```

### Creating and auto-refreshing & cached `TokenProvider`

You can use `TokenProvider.cached` to create an auto-refreshing & cached
version of any `TokenProvider` that will cache each token generated for
the lifespan of that token and then generates a new one.

```scala mdoc:silent
import com.permutive.gcp.auth.TokenProvider

val tokenProvider =
TokenProvider.userAccount[IO](httpClient)

TokenProvider.cached[IO]
.safetyPeriod(4.seconds) // 1.
.onRefreshFailure { case (_, _) => IO.unit }
.onExhaustedRetries(_ => IO.unit)
.onNewToken { case (_, _) => IO.unit }
.retryPolicy(constantDelay[IO](200.millis)) // 2.
.build(tokenProvider)

/**
* 1. How much time less than the indicated expiry to
* cache a token for
* 2. Defaults to 5 retries with a delay between each
* of 200 milliseconds.
*/
```

### Creating an auto-authenticated http4s `Client`

Once you have a `TokenProvider` created, you can use its `clientMiddleware`
method to wrap an http4s' `Client` ensuring every request coming out from it
will contain an `Authorization` header with the access token provided by the
`TokenProvider`.

```scala mdoc:silent
import com.permutive.gcp.auth.TokenProvider

TokenProvider
.userAccount[IO](httpClient)
.map(_.clientMiddleware(httpClient))
```

### Loading a different `TokenProvider` depending on the environment with [pureconfig]

The library also provides a [pureconfig] integration that simplifies the process
of using a different `TokenProvider` on different environments. For example, you
may want to use the workload service-account when running from GCP, but would
want to use a user-account when running your service locally, or use a no-op
access token when running in tests. You can simplify that process by loading
the appropriate `TokenProvider` using pureconfig:

1. Add the following line to your `build.sbt` file:

```sbt
libraryDependencies += "@ORGANIZATION@" %% "@NAME@-pureconfig" % "@VERSION@"
```

2. Use the following type in your configuration class:

```scala mdoc:reset:silent
import com.permutive.gcp.auth.pureconfig._

case class Config(tokenType: TokenType)
```

3. In your `application.conf` file provide the appropriate type:

```conf
token-type = "user-account"
token-type = "service-account"
token-type = "no-op"
```

4. When you want to instantiate your `TokenProvider` simply use:

```scala mdoc:invisible
import cats.effect.IO
import cats.effect.Resource

import org.http4s.client.Client
import org.http4s.Response

val httpClient: Client[IO] = Client[IO] { _ => Resource.pure(Response[IO]())}
val config = Config(TokenType.UserAccount)
```

```scala mdoc:silent
val tokenProvider = config.tokenType.tokenProvider(httpClient)
```

## Contributors to this project

@CONTRIBUTORS_TABLE@

[Google OAuth 2.0]: https://developers.google.com/identity/protocols/OAuth2
[`TokenProvider`]: modules/google-auth/src/main/scala/com/permutive/google/auth/TokenProvider.scala
[Google Service Account Token]: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
[Google User Account Token]: https://developers.google.com/identity/protocols/OAuth2WebServer
[Identity Token]: https://cloud.google.com/run/docs/securing/service-identity#fetching_identity_and_access_tokens_using_the_metadata_server
[instance metadata API]: https://cloud.google.com/compute/docs/access/authenticate-workloads
[pureconfig]: https://pureconfig.github.io
8 changes: 8 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ lazy val documentation = project
.dependsOn(`gcp-auth`, `gcp-auth-pureconfig`)

lazy val `gcp-auth` = module
.settings(libraryDependencies += "com.auth0" % "java-jwt" % "4.4.0")
.settings(libraryDependencies += "com.github.jwt-scala" %% "jwt-circe" % "9.4.5")
.settings(libraryDependencies += "com.permutive" %% "refreshable" % "1.1.0")
.settings(libraryDependencies += "org.http4s" %% "http4s-client" % "0.23.24")
.settings(libraryDependencies += "org.http4s" %% "http4s-circe" % "0.23.24")
.settings(libraryDependencies += "com.alejandrohdezma" %% "http4s-munit" % "0.15.1" % Test)

lazy val `gcp-auth-pureconfig` = module
.settings(libraryDependencies += "com.github.pureconfig" %% "pureconfig-core" % "0.17.4")
.settings(libraryDependencies += "com.alejandrohdezma" %% "http4s-munit" % "0.15.1" % Test)
.dependsOn(`gcp-auth`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2024 Permutive Engineering <https://permutive.com>
*
* 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 com.permutive.gcp.auth.pureconfig

import cats.effect.Concurrent
import cats.syntax.all._

import _root_.pureconfig.ConfigReader
import com.permutive.gcp.auth.TokenProvider
import com.permutive.gcp.auth.models.AccessToken
import fs2.io.file.Files
import org.http4s.client.Client

/** Provides a convenient way to initialise a [[com.permutive.gcp.auth.TokenProvider TokenProvider]] using pureconfig.
*
* Allowed values when reading from configuration files are `user-account`, `service-account` and `no-op`. Check
* [[TokenType.tokenProvider]] for more information on how a [[com.permutive.gcp.auth.TokenProvider TokenProvider]] is
* created from these values.
*/
sealed trait TokenType {

/** Creates a [[com.permutive.gcp.auth.TokenProvider TokenProvider]] using a different method depending on the
* instance:
*
* - [[TokenType.UserAccount]]: the provider will be created using `TokenProvider.userAccount`.
* - [[TokenType.ServiceAccount]]: the provider will be created using `TokenProvider.serviceAccount`.
* - [[TokenType.NoOp]]: will return a provider that always returns
* [[com.permutive.gcp.auth.models.AccessToken.noop AccessToken.noop]].
*/
def tokenProvider[F[_]: Files: Concurrent](httpClient: Client[F]): F[TokenProvider[F]] = this match {
case TokenType.UserAccount => TokenProvider.userAccount[F](httpClient)
case TokenType.ServiceAccount => TokenProvider.serviceAccount[F](httpClient).pure[F]
case TokenType.NoOp => TokenProvider.const(AccessToken.noop).pure[F]
}

}

object TokenType {

case object UserAccount extends TokenType

case object ServiceAccount extends TokenType

case object NoOp extends TokenType

implicit val TokenTypeConfigReader: ConfigReader[TokenType] = ConfigReader.fromStringOpt {
case "user-account" => TokenType.UserAccount.some
case "service-account" => TokenType.ServiceAccount.some
case "no-op" => TokenType.NoOp.some
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"client_id":"my-client-id",
"client_secret":"my-client-secret",
"refresh_token":"refresh_token"
}
Loading

0 comments on commit 22902f7

Please sign in to comment.