fsclient


License

License

Categories

Categories

CLI User Interface
GroupId

GroupId

io.bartholomews
ArtifactId

ArtifactId

fsclient_2.13
Last Version

Last Version

0.1.0
Release Date

Release Date

Type

Type

jar
Description

Description

fsclient
fsclient
Project URL

Project URL

https://github.com/bartholomews/fsclient
Project Organization

Project Organization

io.bartholomews
Source Code Management

Source Code Management

https://github.com/bartholomews/fsclient

Download fsclient_2.13

How to add to project

<!-- https://jarcasting.com/artifacts/io.bartholomews/fsclient_2.13/ -->
<dependency>
    <groupId>io.bartholomews</groupId>
    <artifactId>fsclient_2.13</artifactId>
    <version>0.1.0</version>
</dependency>
// https://jarcasting.com/artifacts/io.bartholomews/fsclient_2.13/
implementation 'io.bartholomews:fsclient_2.13:0.1.0'
// https://jarcasting.com/artifacts/io.bartholomews/fsclient_2.13/
implementation ("io.bartholomews:fsclient_2.13:0.1.0")
'io.bartholomews:fsclient_2.13:jar:0.1.0'
<dependency org="io.bartholomews" name="fsclient_2.13" rev="0.1.0">
  <artifact name="fsclient_2.13" type="jar" />
</dependency>
@Grapes(
@Grab(group='io.bartholomews', module='fsclient_2.13', version='0.1.0')
)
libraryDependencies += "io.bartholomews" % "fsclient_2.13" % "0.1.0"
[io.bartholomews/fsclient_2.13 "0.1.0"]

Dependencies

compile (9)

Group / Artifact Type Version
org.scala-lang : scala-library jar 2.13.3
ch.qos.logback : logback-classic jar 1.2.3
ch.qos.logback : logback-core jar 1.2.3
com.github.pureconfig : pureconfig_2.13 jar 0.14.0
com.github.pureconfig : pureconfig-cats-effect_2.13 jar 0.14.0
com.softwaremill.sttp.client : core_2.13 jar 2.2.9
com.softwaremill.sttp.client : circe_2.13 jar 2.2.9
io.circe : circe-generic-extras_2.13 jar 0.13.0
com.beachape : enumeratum-circe_2.13 jar 1.6.1

test (3)

Group / Artifact Type Version
org.scalactic : scalactic_2.13 jar 3.2.3
org.scalatest : scalatest_2.13 jar 3.2.3
com.github.tomakehurst : wiremock jar 2.27.2

Project Modules

There are no modules declared in this project.

CircleCI codecov Scala Steward badge License: Unlicense

fsclient

Maven Central

libraryDependencies += "io.bartholomews" %% "fsclient-circe" % "0.1.0"

You can also just use the core version, in that case you might need to provide your own codecs
(e.g. to decode an OAuth2 token response):

libraryDependencies += "io.bartholomews" %% "fsclient-core" % "0.1.0"

http client wrapping sttp and providing OAuth signatures and other utils

  import io.bartholomews.fsclient.core._
  import io.bartholomews.fsclient.core.oauth.Signer
  import sttp.client3._

  implicit val signer: Signer = ???

  /*
    Sign the sttp request with `Signer`, which might be one of:
    - an OAuth v1 signature
    - an OAuth v2 basic / bearer
    - a custom `Authorization` header
   */
  emptyRequest
    .get(uri"https://some-server/authenticated-endpoint")
    .sign

OAuth 1.0

Token Credentials

  import io.bartholomews.fsclient.client.ClientData.sampleRedirectUri
  import io.bartholomews.fsclient.core.config.UserAgent
  import io.bartholomews.fsclient.core.oauth.v1.OAuthV1.{Consumer, SignatureMethod}
  import io.bartholomews.fsclient.core.oauth.v1.TemporaryCredentials
  import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.RedirectUri
  import io.bartholomews.fsclient.core.oauth.{
    RequestTokenCredentials,
    ResourceOwnerAuthorizationUri,
    TemporaryCredentialsRequest
  }
  import sttp.client3.{
    emptyRequest,
    DeserializationException,
    HttpURLConnectionBackend,
    Identity,
    Response,
    ResponseException,
    SttpBackend,
    UriContext
  }
  import sttp.model.Method

  type F[X] = Identity[X]

  def dealWithIt = throw new Exception("¯x--(ツ)--x")

  implicit val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()

  val userAgent = UserAgent(
    appName = "SAMPLE_APP_NAME",
    appVersion = Some("SAMPLE_APP_VERSION"),
    appUrl = Some("https://bartholomews.io/sample-app-url")
  )

  // you probably want to load this from config
  val myConsumer: Consumer = Consumer(
    key = "CONSUMER_KEY",
    secret = "CONSUMER_SECRET"
  )

  val myRedirectUri = RedirectUri(uri"https://my-app/callback")

  // 1. Prepare a temporary credentials request
  val temporaryCredentialsRequest = TemporaryCredentialsRequest(
    myConsumer,
    myRedirectUri,
    SignatureMethod.SHA1
  )

  // 2. Retrieve temporary credentials
  val maybeTemporaryCredentials: F[Response[Either[ResponseException[String, Exception], TemporaryCredentials]]] =
    temporaryCredentialsRequest.send(
      Method.POST,
      // https://tools.ietf.org/html/rfc5849#section-2.1
      serverUri = uri"https://some-authorization-server/oauth/request-token",
      userAgent,
      // https://tools.ietf.org/html/rfc5849#section-2.2
      ResourceOwnerAuthorizationUri(uri"https://some-server/oauth/authorize")
    )

  // a successful `resourceOwnerAuthorizationUriResponse` will have the token in the query parameters:
  val resourceOwnerAuthorizationUriResponse =
    sampleRedirectUri.value.withParams(("oauth_token", "AAA"), ("oauth_verifier", "ZZZ"))

  // 3. Get the Token Credentials
  val maybeRequestTokenCredentials: Either[DeserializationException[Exception], RequestTokenCredentials] =
    RequestTokenCredentials.fetchRequestTokenCredentials(
      resourceOwnerAuthorizationUriResponse,
      maybeTemporaryCredentials.body.getOrElse(dealWithIt),
      SignatureMethod.PLAINTEXT
    )

  implicit val requestToken: RequestTokenCredentials = maybeRequestTokenCredentials.getOrElse(dealWithIt)

  // 4. Use the Token Credentials
  import io.bartholomews.fsclient.core._
  emptyRequest
    .get(uri"https://some-server/authenticated-endpoint")
    .sign // sign with the implicit token provided
}

OAuth 2.0

Client credentials

  import io.bartholomews.fsclient.core.oauth.NonRefreshableTokenSigner
  import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.ClientCredentialsGrant
  import io.bartholomews.fsclient.core.oauth.v2.{ClientId, ClientPassword, ClientSecret}
  import io.circe
  import sttp.client3.{HttpURLConnectionBackend, Identity, Response, ResponseException, SttpBackend, UriContext}

  type F[X] = Identity[X]

  implicit val backend: SttpBackend[F, Any] = HttpURLConnectionBackend()

  // using fsclient-circe codecs
  import io.bartholomews.fsclient.circe._

  // you probably want to load this from config
  val myClientPassword = ClientPassword(
    clientId = ClientId("APP_CLIENT_ID"),
    clientSecret = ClientSecret("APP_CLIENT_SECRET")
  )

  val accessTokenRequest: F[Response[Either[ResponseException[String, circe.Error], NonRefreshableTokenSigner]]] =
    backend.send(
      ClientCredentialsGrant
        .accessTokenRequest(
          serverUri = uri"https://some-authorization-server/token",
          myClientPassword
        )
    )

Implicit grant

  import io.bartholomews.fsclient.core.FsClient
  import io.bartholomews.fsclient.core.config.UserAgent
  import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.{ImplicitGrant, RedirectUri}
  import io.bartholomews.fsclient.core.oauth.v2.{AuthorizationTokenRequest, ClientId, ClientPassword, ClientSecret}
  import io.bartholomews.fsclient.core.oauth.{ClientPasswordAuthentication, NonRefreshableTokenSigner}
  import sttp.client3.{emptyRequest, HttpURLConnectionBackend, Identity, SttpBackend, UriContext}
  import sttp.model.Uri

  def dealWithIt = throw new Exception("¯x--(ツ)--x")

  implicit val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()

  val userAgent: UserAgent = UserAgent(
    appName = "SAMPLE_APP_NAME",
    appVersion = Some("SAMPLE_APP_VERSION"),
    appUrl = Some("https://bartholomews.io/sample-app-url")
  )

  // you probably want to load this from config
  val myClientPassword: ClientPassword = ClientPassword(
    clientId = ClientId("APP_CLIENT_ID"),
    clientSecret = ClientSecret("APP_CLIENT_SECRET")
  )

  val myRedirectUri = RedirectUri(uri"https://my-app/callback")

  val client = FsClient.v2.clientPassword(userAgent, ClientPasswordAuthentication(myClientPassword))

  // 1. Prepare an authorization token request
  val authorizationTokenRequest = AuthorizationTokenRequest(
    clientId = client.signer.clientPassword.clientId,
    redirectUri = myRedirectUri,
    state = Some("some-state"), // see https://tools.ietf.org/html/rfc6749#section-10.12
    scopes = List.empty // see https://tools.ietf.org/html/rfc6749#section-3.3
  )

  /*
     2. Send the user to `authorizationRequestUri`,
     where they will accept/deny permissions for our client app to access their data;
     they will be then redirected to `authorizationCodeRequest.redirectUri`
   */
  val authorizationRequestUri: Uri = ImplicitGrant.authorizationRequestUri(
    request = authorizationTokenRequest,
    serverUri = uri"https://some-authorization-server/authorize"
  )

  // a successful `redirectionUriResponse` will have the token in the query parameters:
  val redirectionUriResponseApproved =
    uri"https://my-app/callback?access_token=some-token-verifier&token_type=bearer&state=some-state"

  // 4. Get an access token
  val maybeToken: Either[String, NonRefreshableTokenSigner] = ImplicitGrant
    .accessTokenResponse(
      request = authorizationTokenRequest,
      redirectionUriResponse = redirectionUriResponseApproved
    )

  // 5. Use the access token
  import io.bartholomews.fsclient.core._
  emptyRequest
    .get(uri"https://some-server/authenticated-endpoint")
    .sign(maybeToken.getOrElse(dealWithIt)) // sign with the implicit token provided

Authorization code grant

  import io.bartholomews.fsclient.core.FsClient
  import io.bartholomews.fsclient.core.config.UserAgent
  import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.{AuthorizationCodeGrant, RedirectUri}
  import io.bartholomews.fsclient.core.oauth.v2.{
    AuthorizationCode,
    AuthorizationCodeRequest,
    ClientId,
    ClientPassword,
    ClientSecret
  }
  import io.bartholomews.fsclient.core.oauth.{AccessTokenSigner, ClientPasswordAuthentication}
  import sttp.client3.{HttpURLConnectionBackend, Identity, ResponseException, SttpBackend, UriContext}
  import sttp.model.Uri

  def dealWithIt = throw new Exception("¯x--(ツ)--x")

  implicit val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()

  val userAgent = UserAgent(
    appName = "SAMPLE_APP_NAME",
    appVersion = Some("SAMPLE_APP_VERSION"),
    appUrl = Some("https://bartholomews.io/sample-app-url")
  )

  // you probably want to load this from config
  val myClientPassword = ClientPassword(
    clientId = ClientId("APP_CLIENT_ID"),
    clientSecret = ClientSecret("APP_CLIENT_SECRET")
  )

  val myRedirectUri = RedirectUri(uri"https://my-app/callback")

  val client = FsClient.v2.clientPassword(userAgent, ClientPasswordAuthentication(myClientPassword))

  // 1. Prepare an authorization code request
  val authorizationCodeRequest = AuthorizationCodeRequest(
    clientId = client.signer.clientPassword.clientId,
    redirectUri = myRedirectUri,
    state = Some("some-state"), // see https://tools.ietf.org/html/rfc6749#section-10.12
    scopes = List.empty // see https://tools.ietf.org/html/rfc6749#section-3.3
  )

  /*
     2. Send the user to `authorizationRequestUri`,
     where they will accept/deny permissions for our client app to access their data;
     they will be then redirected to `authorizationCodeRequest.redirectUri`
   */
  val authorizationRequestUri: Uri = AuthorizationCodeGrant.authorizationRequestUri(
    request = authorizationCodeRequest,
    serverUri = uri"https://some-authorization-server/authorize"
  )

  // a successful `redirectionUriResponse` will look like this:
  val redirectionUriResponseApproved = uri"https://my-app/callback?code=some-auth-code-verifier&state=some-state"

  // 3. Validate `redirectionUriResponse`
  val maybeAuthorizationCode: Either[String, AuthorizationCode] = AuthorizationCodeGrant.authorizationResponse(
    request = authorizationCodeRequest,
    redirectionUriResponse = redirectionUriResponseApproved
  )

  // using fsclient-circe codecs
  import io.bartholomews.fsclient.circe._

  // 4. Get an access token
  val maybeToken: Either[ResponseException[String, io.circe.Error], AccessTokenSigner] =
    backend
      .send(
        AuthorizationCodeGrant
          .accessTokenRequest[io.circe.Error](
            serverUri = uri"https://some-authorization-server/token",
            code = maybeAuthorizationCode.getOrElse(dealWithIt),
            maybeRedirectUri = Some(myRedirectUri),
            clientPassword = myClientPassword
          )
      )
      .body

  implicit val accessToken: AccessTokenSigner = maybeToken.getOrElse(dealWithIt)

  // 5. Use the access token
  import io.bartholomews.fsclient.core._
  // an empty request with client `User-Agent` header
  baseRequest(client)
    .get(uri"https://some-server/authenticated-endpoint")
    .sign // sign with the implicit token provided

  // 6. Get a refresh token
  if (accessToken.isExpired() && accessToken.refreshToken.isDefined) {
    backend.send(
      AuthorizationCodeGrant
        .refreshTokenRequest(
          serverUri = uri"https://some-authorization-server/refresh",
          accessToken.refreshToken.getOrElse(dealWithIt),
          scopes = accessToken.scope.values,
          clientPassword = myClientPassword
        )
    )
  }

CircleCI deployment

Verify local configuration

https://circleci.com/docs/2.0/local-cli/

circleci config validate

CI/CD Pipeline

This project is using sbt-ci-release plugin:

  • Every push to master will trigger a snapshot release.

  • In order to trigger a regular release you need to push a tag:

    ./scripts/release.sh v1.0.0
  • If for some reason you need to replace an older version (e.g. the release stage failed):

    TAG=v1.0.0
    git push --delete origin ${TAG} && git tag --delete ${TAG} \
    && ./scripts/release.sh ${TAG}

Versions

Version
0.1.0
0.0.3
0.0.2
0.0.1