// circe codecs
libraryDependencies += "io.bartholomews" %% "fsclient-circe" % "0.1.2"
// play-json codecs
libraryDependencies += "io.bartholomews" %% "fsclient-play" % "0.1.2"
// no codecs
libraryDependencies += "io.bartholomews" %% "fsclient-core" % "0.1.2"
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._
val token: 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(token)
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.{
RedirectUri,
RequestTokenCredentials,
ResourceOwnerAuthorizationUri,
TemporaryCredentialsRequest
}
import sttp.client3.{
emptyRequest,
HttpURLConnectionBackend,
Identity,
Response,
ResponseException,
SttpBackend,
UriContext
}
import sttp.model.Method
// Choose your effect / sttp backend
type F[X] = Identity[X]
val backend: SttpBackend[F, 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")
)(backend)
// a successful `resourceOwnerAuthorizationUriResponse` will have the token in the query parameters:
val resourceOwnerAuthorizationUriResponse =
myRedirectUri.value.withParams(("oauth_token", "AAA"), ("oauth_verifier", "ZZZ"))
// 3. Extract the Token Credentials
val maybeRequestTokenCredentials: Either[ResponseException[String, Exception], RequestTokenCredentials] =
maybeTemporaryCredentials.body.flatMap { temporaryCredentials =>
RequestTokenCredentials.fetchRequestTokenCredentials(
resourceOwnerAuthorizationUriResponse,
temporaryCredentials,
SignatureMethod.PLAINTEXT
)
}
maybeRequestTokenCredentials.map { token =>
// import `FsClientSttpExtensions` in http package to use `sign`
import io.bartholomews.fsclient.core._
// 4. Use the Token Credentials
emptyRequest
.get(uri"https://some-server/authenticated-endpoint")
.sign(token)
}
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]
val backend: SttpBackend[F, Any] = HttpURLConnectionBackend()
// using fsclient-circe codecs, you could also use play-json or provide your own
import io.bartholomews.fsclient.circe.codecs._
// 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
)
)
import io.bartholomews.fsclient.core.FsClient
import io.bartholomews.fsclient.core.config.UserAgent
import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.ImplicitGrant
import io.bartholomews.fsclient.core.oauth.v2.{AuthorizationTokenRequest, ClientId, ClientPassword, ClientSecret}
import io.bartholomews.fsclient.core.oauth.{ClientPasswordAuthentication, NonRefreshableTokenSigner, RedirectUri}
import sttp.client3.{emptyRequest, HttpURLConnectionBackend, Identity, SttpBackend, UriContext}
import sttp.model.Uri
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))(backend)
// 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 `AuthorizationTokenRequest.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
)
maybeToken.map { token =>
// import `FsClientSttpExtensions` in http package to use `sign`
import io.bartholomews.fsclient.core._
// 5. Use the access token
emptyRequest
.get(uri"https://some-server/authenticated-endpoint")
.sign(token) // sign with the token provided
}
import io.bartholomews.fsclient.core._
import io.bartholomews.fsclient.core.config.UserAgent
import io.bartholomews.fsclient.core.oauth.v2.OAuthV2.{AuthorizationCodeGrant, RefreshToken}
import io.bartholomews.fsclient.core.oauth.v2._
import io.bartholomews.fsclient.core.oauth.{AccessTokenSigner, ClientPasswordAuthentication, RedirectUri}
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext}
import sttp.model.Uri
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))(backend)
// 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.codecs._
// 4. Get an access token
val maybeToken: Either[String, AccessTokenSigner] =
maybeAuthorizationCode.flatMap { authorizationCode =>
backend
.send(
AuthorizationCodeGrant
.accessTokenRequest(
serverUri = uri"https://some-authorization-server/token",
code = authorizationCode,
maybeRedirectUri = Some(myRedirectUri),
clientPassword = myClientPassword
)
)
.body
.left
.map(_.getMessage)
}
maybeToken.map { accessTokenSigner =>
// 5. Use the access token
baseRequest(userAgent)
.get(uri"https://some-server/authenticated-endpoint")
.sign(accessTokenSigner) // sign with the token signer
// 6. Get a refresh token
if (accessTokenSigner.isExpired()) {
backend.send(
AuthorizationCodeGrant
.refreshTokenRequest(
serverUri = uri"https://some-authorization-server/refresh",
accessTokenSigner.refreshToken.getOrElse(
RefreshToken(
"Refresh token is optional: some implementations (e.g. Spotify) only give you a refresh token " +
"with the first `accessTokenSigner` authorization response, so you might need to store and use that."
)
),
scopes = accessTokenSigner.scope.values,
clientPassword = myClientPassword
)
)
}
}
https://circleci.com/docs/2.0/local-cli/
circleci config validate
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}