fsclient
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}