Testing presents special challenges for packages that wrap an API. Here we tackle one of those problems: how to deal with auth in a non-interactive setting on a remote machine. This affects gargle itself and will affect any client package that relies on gargle for auth.
This article documents the token management approach taken in gargle. We wanted it to be relatively easy to have a secret, such as an auth token, that we can:
all while keeping the secret secure.
The approach uses symmetric encryption, where the shared key is stored in an environment variable. Why? This works well with existing conventions for local R usage. Most CI services offer support for secure environment variables. And R-hub accepts environment variables via the
env_vars argument of
This is based on an approach originally worked out in bigrquery.
gargle’s approach to managing test tokens is implemented through several functions that all start with the
secret_ prefix. These functions are not (currently?) exported. This may seem odd, since others might want to use these functions. But note they are only needed during setup or at test time. This sort of usage is compatible with others calling internal gargle functions and possibly inlining a version of a couple test helpers.
One way to make the
secret_*() functions available for local experimentation is to call
devtools::load_all(), which exposes all internal objects in a package:
The approach I’ll take in this article is to call these functions via
usethis::use_package("sodium", "Suggests")if you like.
GARGLE_PASSWORD. Store as an environment variable.
secret_pw_name() creates a name of the form “PACKAGE_PASSWORD”, a convention baked into the
secret_*() family of functions.
In real life, you should keep the output of
secret_pw_gen() to yourself! We reveal it here as part of the exposition.
Combine the name and value to form a line like this in your user-level
usethis::edit_r_environ() can help create or open this file. We strongly recommend using the user-level
.Renviron, as opposed to project-level, because this makes it less likely you will share sensitive information by mistake. If you don’t take our advice and choose to store the PASSWORD in a file inside a Git repo, you must make sure that file is listed in
.gitignore. This still would not prevent leaking your secret if, for example, that project is in a directory that syncs to DropBox.
.Renviron ends in a newline; the lack of this is a notorious cause of silent failure. Remember you’ll need to restart R or call
readRenviron("~/.Renviron") for the newly defined environment variable to take effect.
Define the environment variable in your repo settings via the browser UI:
Alternatively, you can use the Travis command line interface to configure the environment variable or even define an encrypted environment variable in
Regardless of how you define it, remember that private environment variables are not available to external pull requests, which is another reason to carry on gracefully when token decryption is not possible (see below).
Define the environment variable in the Environment page of your repo’s Settings. Make sure to request variable encryption and to click “Save” at the bottom. In the General page, you probably want to check “Enable secure variables in Pull Requests from the same repository only” and, again, explicitly “Save”.
As with Travis, it is also possible to encrypt the password using your AppVeyor account’s public key and inline the value in
appveyor.yml. There is a helpful web UI for that does the encryption and generates the lines to add to your config:
This can also be found via Settings > Encrypt YAML.
secret_write() takes 3 arguments:
packagename. Processed through
secret_pw_name()in order to retrieve the PASSWORD from an appropriately named environment variable.
nameof the encrypted file to write. The location is below
inst/secretin the source of
data, either a file path to the unencrypted secret file or the data to be encrypted as a raw vector. In the case of a secret file, we strongly recommend that its primary home on your local computer is outside your package and, generally, outside of any folder that syncs regularly to a remote, e.g. GitHub or DropBox. This decreases the chance of accidental leakage.
Example of a call to
gargle-testing.json is a JSON file downloaded for a service account managed via the Google API / Cloud Platform console:
This writes an encrypted version of
inst/secret/gargle-testing.json relative to the current working directory, which is presumably the top-level directory of gargle’s source. This encrypted file should be committed and pushed.
Now you need to rig your tests or their setup around this encrypted token. You need to plan for two scenarios:
Decryption is going to work. This is where you actually get to test package functionality against the target API. Put this in
.travis.yml in order to make sodium available:
We recommend that you actively check your package under these conditions, so that you discover problems before CRAN or your contributors do. Here’s a simplified excerpt from
.travis.yml where the main
r: release build accesses
GARGLE_PASSWORD implicitly as an encrypted environment variable, but
R CMD check runs for the other builds with
GARGLE_PASSWORD explicitly unset:
In a wrapper package, you could determine decrypt-ability at the start of the test run. Here’s representative code from googledrive’s
tests/testthat/helper.R file, but something similar can be seen in bigrquery and googlesheets4:
secret_read() are defined here in gargle.
drive_auth() is a function specific to googledrive that loads a token for use downstream (in multiple tests, in this case). Note that it can clearly accept a JSON string, as an alternative to a filepath, and that’s very favorable for our workflow. We’ll come back to this below.
But what if
FALSE and no token is loaded? That’s where you rely on a custom test skipper. Here is the test skipper from googledrive:
TRUE if a token is available and
FALSE otherwise. By calling the skipper at the start of tests that require auth, you arrange for your package to cope gracefully when the token cannot be decrypted, e.g., on CRAN and in pull requests. It is typical to define such a skipper in
tests/testthat/helper.R or similar.
gargle’s usage of the testing token is a bit different, still evolving, and less relevant to the maintainers of wrapper packages. Therefore it’s not featured here.
Once you dig into the
secret_*() family, you will notice there are two recurring sources of friction:
Functions useful for these conversions:
bigrquery and googledrive, which both use this approach.
“Managing secrets” vignette of httr:
Vignettes of the sodium package, especially the parts relating to symmetric encryption:
The cyphr package, which smooths over frictions like those identified above relating to “file vs. object?” and “character vs. raw?”: