Skip to content

The Gmail API is primarily intended for use on behalf of a regular Google user account, as opposed to a service account. The gmailr package guides an interactive R user through a process in which they authenticate themselves to Google and authorize Gmail activities initiated from R. This is sometimes referred to as the “OAuth dance”.

But what about settings where there is no interactive user sitting around, i.e. when gmailr-using code is deployed to a remote server or otherwise runs unattended? For most Google APIs, the standard advice is “use a service account”. But the Gmail API is special. To use a service account with the Gmail API basically requires that the service account has been delegated domain-wide authority. This is tricky for at least two reasons. First, this is only possible within a Google Workspace. It’s not available to personal Google accounts. Second, most Google Workspace admins will refuse to do this, for security reasons.

Therefore, if you want to deploy a data product that uses gmailr, it’s entirely possible that you really do need to use a user token. This article is about how to prepare a token for use in this scenario.

The instructions below involve filepaths and environment variables. Therefore, you will need to modify the code below to account for the specifics of your situation.

Demo code

gmailr ships with code for a working demo of the approach described here.

writeLines(list.files(
  system.file("deployed-token-demo", package = "gmailr")
))
#> README.md
#> send-email-byo-encrypted-token.Rmd
#> token-setup.R

We will make reference to these files below.

You may also wish to browse these files on GitHub: https://github.com/r-lib/gmailr/tree/main/inst/deployed-token-demo.

Setup: store a token

This process is recorded in the demo file token-setup.R.

First, complete the OAuth dance in your primary, interactive environment as the target user, using the desired OAuth client and scopes, with cache = FALSE. If you have arranged for the desired OAuth client to be discovered via gm_default_oauth_client(), you only need to call gm_auth():

gm_auth("jane@example.com", cache = FALSE)

If you need to specify the OAuth client explicitly, call gm_auth_configure() prior to gm_auth():

gm_auth_configure("path/to/your/oauth_client.json")
gm_auth("jane@example.com", cache = FALSE)

You may wish to confirm that you are logged in as the intended user:

Now, write the current gmailr token to file. If you are deploying to somewhere relatively private, such as a server accessible only within your organization, you don’t need to provide any arguments to gm_token_write(). But you’ll often want to specify the target path:

gm_token_write(path = "path/to/gmailr-token.rds")

The resulting token file is rather opaque, i.e. a general purpose automated tool can’t easily scrape your credentials from this. But a knowledgeable R programmer could decode the token, if they made an effort.

If the token file will be exposed in a more public location, such as on GitHub or inside a CRAN package, it should be encrypted. You can generate an encryption key with gargle::secret_make_key() (this is a copy of httr2::secret_make_key(), which you could also use). In your local development environment, make this key available as an environment variable, e.g. GMAILR_KEY, probably with a line like this in your .Renviron file:

GMAILR_KEY=xxxxxxxxxxxxxxx

The usethis::edit_r_environ() function can be handy for creating and/or editing this file.

Once you’ve set up the encryption key, you can use it in gm_token_write(key =):

gm_token_write(
  path = "path/to/gmailr-token.rds",
  key = "GMAILR_KEY"
)

You must make this same key available as a secret or secure environment variable in the deployed context, e.g. on Posit Connect (https://docs.posit.co/connect/admin/security/#application-environment-variables) or GitHub Actions (https://docs.github.com/en/actions/security-guides/encrypted-secrets).

Usage: load and use token

The demo file send-email-byo-encrypted-token.Rmd is an example of a working Shiny app (Shiny document, in this case) that implements the technique described here..

In the code that’s running in the deployed / unattended setting, use a snippet like this to read the token from file and tell gmailr to use it:

gm_auth(token = gm_token_read(
  path = "path/to/gmailr-token.rds",
  key = "GMAILR_KEY"
))

If you did not specify the key in gm_token_write(), omit it from the gm_token_read() call as well. If you did specify the key in gm_token_write(), use the same key in gm_token_read().

Ongoing maintenance

The saved credential contains a refresh token, which is potentially rather long-lived, but is still perishable. As long as the refresh token remains valid, it can be used to obtain short-lived access tokens, without any user interaction. This is sometimes referred to as “refreshing the token” and this is what’s happening behind the scenes with a deployed token.

However, there are many ways that the refresh token can become invalid, for example:

  • In the Security settings for their Google user account, a user can remove access associated with a specific OAuth client or app. This invalidates any token obtained with that client.
  • If a token isn’t used for a period of time (~6 months), it becomes invalid.
  • If an OAuth client is deleted or its host project disables the Gmail API, any associated tokens become invalid.
  • There’s a limit to how many refresh tokens a user can have. If a user repeatedly mints new tokens (versus refreshing existing ones), older tokens will “fall off the end” and become invalid.
  • If an OAuth client is in “testing” mode, all associated tokens have a limited lifetime, usually 1 week.

The general topic of refresh token expiration is documented in https://developers.google.com/identity/protocols/oauth2#expiration.

If the token becomes invalid, token refresh will fail and your deployed product will no longer be able to access the Gmail API on behalf of the target user. It is a very good idea to rig your code to surface this failure in a very transparent way, so it’s easier for you to diagnose this problem. Functions like gm_profile(), gargle::token_tokeninfo(), and gargle::token_userinfo() can be useful for this. If the stored token can no longer be refreshed, the only remedy is to obtain, store, possibly encrypt, and deploy a new token, using the exact same process as before.