Closed tvroylandt closed 1 year ago
Hi @jennybc & @jimhester,
I am still unable to get a Service Account working with gmailr
so I thought I'd ask for assistance again. I raised the issue on the wrong thread (#144), for which @jennybc responded and answered all of the related issues (Thank you!) though I was not able to figure out how to authorize gmailr
with a service account from the advice provided there. I'm trying again and this is what I have thus far:
A Service account is created on a standard/non-workspace Google account and the json is downloaded.
I've attempted to authorize with the following code derived from the docs here:
token <- gargle::token_fetch(
scopes = c("https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.send"),
path = "inst/auth/rminor@rminor-333915.iam.gserviceaccount.com.json")
config <- gmailr::gm_auth_configure(app = token)
gmailr::gm_auth(token = config,
cache = FALSE,
email = "rminor@rminor-333915.iam.gserviceaccount.com")
I get this error message from gmailr::gm_auth_configure
:
Error in if (!have_app) { : missing value where TRUE/FALSE needed
and this from gm_auth
:
Error: Must create an app and register it with `gm_auth_configure()`
I created an Oauth consent for testing as I thought that may be an issue:
but that did not resolve it.
I've tried passing the Service Account json directly to path
as one would do with googlesheets4
:
gmailr::gm_auth_configure(path = "inst/auth/rminor@rminor-333915.iam.gserviceaccount.com")
but this results in the following error:
Error: Can't find client_id and client_secret in the JSON.
If one of y'all has some time could you provide some guidance for myself and others on this thread (or whoever may encounter it) as to how to get a service account and gmailr
working together? Or is it not possible because the Service account doesn't have an associated email inbox?
Thanks in advance, Stephen
Hi gmailr
devs,
After exploring the gm_auth_configure
it looks like it can be circumvented and the authorization credential can be registered directly with the following:
assign("cred",gargle::token_fetch(path = "google_SA.json"), gmailr:::.auth)
This allowed me to start sending requests to the API. I tried to send a test email and received the following error message:
Error in gmailr_POST(c("messages", "send"), user_id, class = "gmail_message", :
Gmail API error: 403
Gmail API has not been used in project 392021992927 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/gmail.googleapis.com/overview?project=xxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
I went ahead and enabled the Gmail API and tried again.
The error message changed to:
Error in gmailr_POST(c("messages", "send"), user_id, class = "gmail_message", : Gmail API error: 400
I think we might need to enable gmail domain-wide and delegate permissions to the Service Account for the Gmail API to resolve this. We're going to try this. I am wondering if we're barking up the right tree here though?
Hi @yogat3ch, thank you very much for this post. I am building a Shiny App and I use the gmailr package to send emails in production. So, in my case, the service account method is the way to go. As you initially pointed out, the service account credential worked for both googledrive and googlesheets4 using drive_auth() and gs4_auth() functions. However, for the gmailr package is not as straightforward, I've had the same error as you, given that in this case, it is expected to use gm_auth_configure() first and then gm_auth(), stating that the way to go is the OAuth method. @jennybc & @jimhester this should be corrected, the main issue is with the function gm_oauth_app():
gm_oauth_app <- function ()
{
if (!is.null(.auth$app)) {
return(.auth$app)
}
stop("Must create an app and register it with `gm_auth_configure()`",
call. = FALSE)
}
Please also include in the gm_auth() function, the "subject" parameter as well to allow a service account to impersonate a given Gmail Workspace account.
Here is how I made it work, using @yogat3ch's hack:
json_path <- gargle:::secret_read("your_package", "credential.json")
# Get token given the service account credentials
token <- gargle::credentials_service_account(
scopes = "https://mail.google.com/", # Full scope
path = rawToChar(json_path), # Encrypted credential
subject = get_golem_config("gmail_account") # Allow service account to act on behalf of this subject
)
# Assign token to .auth internal object from gmailr package
assign("cred", token, gmailr:::.auth)
In order to grant domain-wide authority, you need to have an administrator Google Workspace account:
@alexquant1993 thanks for the excellent post. I had been troubleshooting this issue for a while and your solution worked perfectly.
Yeah @alexquant1993 I agree with your diagnosis. The early unqualified call to gm_oauth_app()
is causing an early error for someone trying to use a service account. Creative solution! But yes we should fix it more properly in gmailr.
I'm curious: are you successfully sending email as someone other than the service account this way? This has been reported as "not working" in gargle and I've never truly dug into it because I am not allowed to grant a service account domain wide delegation at my workplace, which makes it really hard to investigate. I have formed a hypothesis is that the problem is specifying the subject's email address vs. an id. What does get_golem_config("gmail_account")
return? Does it look like an email address or a user ID?
A slightly less "exciting" adaptation of @alexquant1993's workaround is more like (warning: code untested):
# decoy oauth app that we won't actually use
gm_auth_configure(key = "PLACEHOLDER", secret = "PLACEHOLDER")
# Get token given the service account credentials
token <- gargle::credentials_service_account(
scopes = "https://mail.google.com/",
path = "path/to/the/json/for/the/service/account.json",
subject = "??" # specifying the subject
)
gm_auth(token = token)
Update: I tried to test that and all of the above works. But I can't truly test it by, say, sending an email, since I don't have a service account with domain wide authority. And I'm almost certain that a service account must be acting on behalf of a subject with the Gmail API.
Even simpler workaround (again: not completely tested since I can't grant a service account domain wide authority)
gm_auth_configure(key = "PLACEHOLDER", secret = "PLACEHOLDER")
gm_auth(
path = "path/to/the/json/for/the/service/account.json",
subject = "??" # specify the subject
)
Update: in hindsight, this can't work because gm_auth()
won't accept subject
.
I'm still trying to figure out exactly what to do here. Jotting down some analysis and notes for myself.
Overall, the magical gargle::token_fetch()
is great, because it heroically tries lots of methods of auth. But the flipside of that coin is that it silently swallows all sorts of errors, some of which would be useful for the user to see.
If we could reliably tell what sort of auth the user wants, it would be possible to determine whether an OAuth client is required. But that's pretty opposed to the design of gm_auth()
and similar functions in other packages.
As it stands, the unconditional call to app <- gm_oauth_app()
makes it extremely clunky (see various creative workarounds above) to auth via any method other than credentials_user_oauth2()
. That is almost certainly the most common method of auth for gmailr, but by default all these other methods are tried first and should be possible to use (up to some other, bigger problems described next):
I have the impression that it's not possible to send email as a service account. Quoting from (https://github.com/googleapis/google-api-nodejs-client/issues/2322#issuecomment-692908531):
The Gmail API isn't intended to be used with service accounts (other than domain-wide delegation use cases.) You need to be acting as a real user -- either using oauth credentials obtained with user consent, or in the case of a Gsuite domain, using a service account delegating/impersonating a real user.
So I think the only Gmail-using scenario where a service account is relevant is when one is also passing the sub
specifying the regular user on whose behalf the service account is operating. Which gmailr::gm_auth()
currently does not support, although I plan to add that.
Therefore, the title of thread is basically true ("Service account authentification doesn't work"), although the root cause isn't really this call to app <- gm_oauth_app()
(although that's problematic). The real blocker is the lack of support for passing the subject parameter along.
With dev gmailr, here is the current best pre-emptive workaround for (theoretically) using one of the auth methods other than credentials_user_oauth2()
inside the gargle::token_fetch()
call, inside gm_auth()
:
fake_client <- gargle::gargle_oauth_client(
id = "PLACEHOLDER",
secret = "PLACEHOLDER"
)
gm_auth_configure(fake_client)
This is necessary, for example, to use credentials_byo_oauth2()
, which is viable today, and is what gmailr uses in its own tests.
Hi,
Thanks for this great package.
I tried to configure
gmailr
using a service account token likegm_auth(path = "my_token.json")
but it send me this error :I know my token is good because when I use it with
googledrive
orgooglesheets4
, it works.I can create an app using OAuth, but I would like to use service account token here.
Thanks