Over the course of several releases (v1.3.0, v1.4.0, and v1.5.0),
gargle has shifted to using an OAuth client in the user
flow facilitated by gargle::credentials_user_oauth2()
,
instead the previous OAuth “app”. This is a more than just a vocabulary
change (but it is also a vocabulary change). This vignette explains what
actually changed and how wrapper packages should adjust.
In 2022, Google partially deprecated the out-of-band (OOB) OAuth flow. The OOB flow is used by R users who are working with Google APIs and who use R in the browser, such as via RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory.
Conventional OOB auth still works under certain conditions, for example, if the OAuth client is associated with a GCP project that is in testing mode or that is internal to a Google Workspace. But conventional OOB is no longer supported for projects that serve external users that are in production mode. In particular, this means that conventional OOB is no longer supported for the GCP project that has historically made auth “just work” for casual users of packages such as googledrive, googlesheets4, and bigrquery. The default OAuth client used by these package no longer works with conventional OOB.
In response, as of v1.3.0, gargle implements a new variant of OOB,
called pseudo-OOB, to continue to provide a
user-friendly auth flow for googledrive/googlesheets4/bigrquery on
RStudio Server/Posit Workbench/Posit Cloud/Google Colaboratory. The
pseudo-OOB flow is also available for other developers to use. This flow
is triggered when use_oob = TRUE
(an existing convention in
gargle and gargle-using packages) and the configured
OAuth client is of the web type (when creating an OAuth client,
this is called the “Web application” type).
FALSE | TRUE | ||
---|---|---|---|
client type | installed | use httpuv to spin up a temporary web server |
conventional OOB |
web | --not possible-- | pseudo-OOB |
In the past, gargle basically assumed that every OAuth client was of
the installed type (when creating an OAuth client, this is
called the “Desktop app” type). Therefore, the introduction of
pseudo-OOB meant that gargle had to learn about different OAuth client
types (web vs. installed). And that didn’t play well with
httr::oauth_app()
, which gargle had been using to store the
client ID and secret.
That’s why there is a new S3 class,
"gargle_oauth_client"
, with a constructor of the same name.
Since more information is now necessary to instantiate a client
(e.g. its type and, potentially, redirect URIs), the recommended way to
create a client is to provide JSON downloaded from the GCP console to
gargle_oauth_client_from_json()
.
Since we had to introduce a new S3 class and supporting functions, we
also took this chance to make the vocabulary pivot from “OAuth app” to
“OAuth client”. Google’s documentation has always talked about the
“OAuth client”, so this is more natural. This vocabulary is also more
future-facing, anticipating the day when gargle might shift from httr to
httr2, which uses httr2:oauth_client()
. As a bridging
measure, the "gargle_oauth_client"
class currently inherits
from httr’s "oauth_app"
, but this probably won’t be true in
the long-term.
If you do auth via gargle, here are some recommended changes:
httr::oauth_app()
or
gargle::oauth_app_from_json()
to instantiate an OAuth
client.gargle_oauth_client_from_json()
(strongly
recommended) or gargle_oauth_client()
instead.This advice applies to anything you do inside your package and also to what you encourage and document for your users.
gargle ships with JSON files for two non-functional OAuth clients, just to make this all more concrete:
(path_to_installed_client <- system.file(
"extdata", "client_secret_installed.googleusercontent.com.json",
package = "gargle"
))
#> [1] "/tmp/RtmpxZ8q3E/Rinstd77421fa6ab/gargle/extdata/client_secret_installed.googleusercontent.com.json"
jsonlite::prettify(scan(path_to_installed_client, what = character()))
#> {
#> "installed": {
#> "client_id": "abc.apps.googleusercontent.com",
#> "project_id": "a_project",
#> "auth_uri": "https://accounts.google.com/o/oauth2/auth",
#> "token_uri": "https://accounts.google.com/o/oauth2/token",
#> "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
#> "client_secret": "ssshh-i-am-a-secret",
#> "redirect_uris": [
#> "http://localhost"
#> ]
#> }
#> }
#>
(client <- gargle_oauth_client_from_json(path_to_installed_client))
#> <gargle_oauth_client>
#> name: a_project_d1c5a8066d2cbe48e8d94514dd286163
#> id: abc.apps.googleusercontent.com
#> secret: <REDACTED>
#> type: installed
#> redirect_uris: http://localhost
class(client)
#> [1] "gargle_oauth_client" "oauth_app"
(path_to_web_client <- system.file(
"extdata", "client_secret_web.googleusercontent.com.json",
package = "gargle"
))
#> [1] "/tmp/RtmpxZ8q3E/Rinstd77421fa6ab/gargle/extdata/client_secret_web.googleusercontent.com.json"
jsonlite::prettify(scan(path_to_web_client, what = character()))
#> {
#> "web": {
#> "client_id": "abc.apps.googleusercontent.com",
#> "project_id": "a_project",
#> "auth_uri": "https://accounts.google.com/o/oauth2/auth",
#> "token_uri": "https://accounts.google.com/o/oauth2/token",
#> "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
#> "client_secret": "ssshh-i-am-a-secret",
#> "redirect_uris": [
#> "https://www.tidyverse.org/google-callback/"
#> ]
#> }
#> }
#>
(client <- gargle_oauth_client_from_json(path_to_web_client))
#> <gargle_oauth_client>
#> name: a_project_d1c5a8066d2cbe48e8d94514dd286163
#> id: abc.apps.googleusercontent.com
#> secret: <REDACTED>
#> type: web
#> redirect_uris: https://www.tidyverse.org/google-callback/
class(client)
#> [1] "gargle_oauth_client" "oauth_app"
Notice the difference in the JSON for the installed vs. web client.
Note the class of the client
object, the new
type
field, and the redirect_uris
.
AuthState
classThere are two gargle classes that are impacted by the
OAuth-app-to-client switch: AuthState
and
Gargle2.0
. We cover AuthState
here and
Gargle2.0
in the next section.
If a wrapper package follows the design laid out in
vignette("gargle-auth-in-client-package")
, it will use an
instance of AuthState
to manage the package’s auth state.
Let’s assume that internal object is named .auth
, which it
usually is. Here are the changes you need to know about in
AuthState
:
app
field is deprecated, in favor of a new field
client
. If you request .auth$app
, there will
be a deprecation message and the client
field is
returned.$set_app()
method is deprecated, in favor of a new
$set_client()
method. If you call
.auth$set_app()
, there will be a deprecation message and
the input is used, instead, to set the client
field.app
argument of the init_AuthState()
constructor is deprecated in favor of the new client
argument. If you call init_AuthState(app = x)
, there will
be a deprecation message and the input x
is used as the
client
argument instead.Here are the changes you probably need to make in your package:
PKG_auth_configure()
, should become client
(which is new). Move the existing app
argument to the last
position and deprecate it.PKG_oauth_app()
(the function to reveal the
user’s configured OAuth client).PKG_oauth_client()
to replace
PKG_oauth_app()
.Here’s how googledrive::drive_auth_configure()
and
googledrive::drive_oauth_client()
looked before and after
the transition:
# BEFORE
drive_auth_configure <- function(app, path, api_key) {
# not showing this code
.auth$set_app(app)
# more code we're not showing
}
drive_oauth_app <- function() .auth$app
# AFTER
drive_auth_configure <- function(client, path, api_key, app = deprecated()) {
if (lifecycle::is_present(app)) {
lifecycle::deprecate_warn(
"2.1.0",
"drive_auth_configure(app)",
"drive_auth_configure(client)"
)
drive_auth_configure(client = app, path = path, api_key = api_key)
}
# not showing this code
.auth$set_client(client)
# more code we're not showing
}
drive_oauth_client <- function() .auth$client
drive_oauth_app <- function() {
lifecycle::deprecate_warn(
"2.1.0", "drive_oauth_app()", "drive_oauth_client()"
)
drive_oauth_client()
}
The approach above follows various conventions explained in
vignette("communicate", package = "lifecycle")
. If you also
choose to use the lifecycle package to assist in this process,
usethis::use_lifecycle()
function does some helpful
one-time setup in your package:
The roxygen documentation helpers in gargle assume
PKG_auth_configure()
is adapted as shown above:
PREFIX_auth_configure_description()
crosslinks to
PREFIX_oauth_client()
now, not
PREFIX_oauth_app()
.PREFIX_auth_configure_params()
documents the
client
argumentPREFIX_auth_configure_params()
uses a lifecycle badge
and text to communicate that app
is deprecated.PREFIX_auth_configure_params()
crosslinks to
gargle::gargle_oauth_client_from_json()
which requires
gargle (>= 1.3.0)Gargle2.0
classGargle2.0
is the second gargle class that is impacted by
the OAuth-app-to-client switch.
Here are the changes you probably need to make in your package:
Inside PKG_auth()
, you presumably call
gargle::token_fetch()
. If you are passing
app = <SOMETHING>
, change that to
client = <SOMETHING>
. Neither app
nor
client
are formal arguments of
gargle::token_fetch()
, instead, these are intended for
eventual use by gargle::credentials_user_oauth2()
. Here’s a
sketch of how this looks in googledrive::drive_auth()
:
drive_auth <- function(...) {
# code not shown
cred <- gargle::token_fetch(
scopes = scopes,
# app = drive_oauth_client() %||% <BUILT_IN_DEFAULT_CLIENT>, # BEFORE
client = drive_oauth_client() %||% <BUILT_IN_DEFAULT_CLIENT>, # AFTER
email = email,
path = path,
package = "googledrive",
cache = cache,
use_oob = use_oob,
token = token
)
# code not shown
}
If you ever call gargle::credentials_user_oauth2()
directly, use the new client
argument instead of the
deprecated app
argument.