Skip to main content
Version: 2.x

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")
}