Authorization with OAuth2
A command-line application interacting with an external API might require some form of authentication, so that access is restricted for unauthorized users. One widely employed authorization and authentication protocol is OAuth2. ZIO CLI can perform interactions with APIs requiring OAuth2. Although it is possible to construct a custom OAuth2 provider easily, there are already tailored OAuth2 providers for GitHub, Google and Facebook within ZIO CLI. We will see how we can add OAuth2
to our CLI application and how we can construct a custom OAuth2 provider to interact with other APIs.
Using OAuth2​
OAuth2 is added to a CLI App as an Options[OAuth2Token]
. The token can be stored and the user can specify the path, so it is not necessary to repeat authentication. We can create it as with other options:
import zio.cli._
import zio.cli.oauth2.OAuth2Provider
import zio.cli.oauth2.OAuth2Provider._
val clientId = "clientId"
val provider: OAuth2Provider = OAuth2Provider.Github(clientId)
val scope: List[String] = List("repo")
val oauth2 = Options.oauth2(provider, scope)
ZIO Sample providers​
Currently the supported OAuth2 providers are GitHub, Google and Facebook.
Github​
To create a GitHub provider, you must specify the clientId
and the scope of the authorization in a list. The ClientId is the client ID received from GitHub during registering.
val githubOAuth = Options.oauth2(OAuth2Provider.Github(clientId), List("repo"))
Google​
Google requires Client ID and a Client Secret generated by Google API Console. They are known both to Google and your application.
val clientSecret = "clientSecret"
val googleOAuth = Options.oauth2(OAuth2Provider.Google(clientId, clientSecret), Nil)
Facebook​
Facebook requires an App Id and Client Token. They can be obtained through a Facebook developer account.
val appId = "appId"
val clientToken = "clientToken"
val facebookOAuth = Options.oauth2(OAuth2Provider.Facebook(appId, clientToken), Nil)
Construction of custom OAuth2 provider​
To create a custom OAuth2 provider, it suffices to extend the trait OAuth2Provider
.
The methods that need to be overrided are the following:
import zio.cli.oauth2.AuthorizationResponse
import java.net.http.HttpRequest
trait OAuth2Provider {
def name: String
def clientIdentifier: String
def authorizationRequest(scope: List[String]): HttpRequest
def accessTokenRequest(authorization: AuthorizationResponse): HttpRequest
def refreshTokenRequest(refreshToken: String): Option[HttpRequest]
}
Two other methods that might need to be overrided depending on the particular provider are
import java.net.http.HttpRequest
import zio.cli.oauth2._
trait OAuth2Provider {
def decodeAuthorizationResponse(body: String): Either[String, AuthorizationResponse]
def decodeAccessTokenResponse(body: String): Either[String, AccessTokenResponse]
}
Description of methods​
- Method
name
It is the name of the provider.
- Method
clientIdentifier
It is a public client identifier as provided after registration on authorization server. It is used for generating default file name, which holds access token.
- Method
authorizationRequest
It generates the HTTP request for authorization request.
- Method
accessTokenRequest
It generates the HTTP request for access token request.
- Method
refreshTokenRequest
It generates the HTTP request for refresh token request. It must return None
if this operation is not supported by the provider.
- Method
decodeAuthorizationResponse
It converts textual response of authorization request into AuthorizationResponse
. It defaults to decoding from standard JSON format.
- Method
decodeAccessTokenResponse
It converts textual response of access token request into AccessTokenResponse
.
It defaults to decoding from standard JSON format.
Construction of GitHub OAuth2 provider​
The construction of an OAuth2 provider will be dependent on the particular API that we would like to access. The first step is to define name
and clientIdentifier
. The value clientIdentifier
can be obtained as a field of the case class representing our Provider. Then we construct the core of the Provider. Observe that the methods authorizationRequest
and accessTokenRequest
construct an HttpRequest
from the library ZIO Http. They represent a POST request to GitHub API.
import zio.cli.oauth2._
import java.net.http.HttpRequest
import java.net.URI
final case class GithubExample(clientId: String) extends OAuth2Provider {
override val name = "Github"
override val clientIdentifier = clientId
// Core logic of Provider
override def authorizationRequest(scope: List[String]): HttpRequest =
HttpRequest
.newBuilder()
.uri(URI.create(s"https://github.com/login/device/code?client_id=$clientId&scope=${scope.mkString(",")}"))
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.noBody())
.build()
override def accessTokenRequest(authorization: AuthorizationResponse): HttpRequest =
HttpRequest
.newBuilder()
.uri(
URI.create(
s"https://github.com/login/oauth/access_token?client_id=$clientId&device_code=${authorization.deviceCode}&grant_type=urn:ietf:params:oauth:grant-type:device_code"
)
)
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.noBody())
.build()
override def refreshTokenRequest(refreshToken: String): Option[HttpRequest] = None
}
Integrating OAuth2​
This example shows how to integrate OAuth2 in a ZIO CliApp
. We are going to make a CLI App that interacts with Github and uploads a file to GitHub (This needs OAuth2).
The first step is to define the Options
that provides an `OAuth2Token
import zio.Console.printLine
import zio.cli.HelpDoc.Span.text
import zio.cli.oauth2.OAuth2Provider
import zio.cli.oauth2._
import zio.cli._
val githubOAuth: Options[OAuth2Token] = Options.oauth2(OAuth2Provider.Github("sampleId"), List("repo"))
Then, we add the token to an Options
providing the path of a file. We construct a command upload
usign this Options
.
val options = Options.file("path") ++ githubOAuth
val upload = Command("upload", options)
Finally, a CliApp
is specified using upload
command.
val cliApp = CliApp.make(
name = "OAuth2 Example",
version = "0.0.1",
summary = text("Example of CliApp with OAuth2"),
command = upload) {
// Implement logic of CliApp
case file => printLine("Upload file to GitHub")
}