r-lib / gmailr

Access the Gmail RESTful API from R.
https://gmailr.r-lib.org
Other
229 stars 56 forks source link

Refresh token automatically #170

Closed KryeKuzhinieri closed 2 years ago

KryeKuzhinieri commented 2 years ago

Hey,

We have a shiny app for which we are using the gmailR package to send emails. We managed to log in and save the token into a .secret folder before deployment. Then, we copied the .secret folder into the app and it worked like a charm. However, it appears that after some time the token expires. Is there anyway to automate this without manually needing to log into the browser and approving the request? Here is the current implementation

options(gargle_verbosity = "debug")
gm_auth_configure(path = "email_creds.json")
gm_auth(
  email = "myemail@gmail.com",
  cache = ".secret",
  scopes = "full"
)
email <- gm_mime(
  from = "myemail@gmail.com",
  to = to,
  subject = "This is a subject",
  body = "This is a body"
)
gm_send_message(email)
jennybc commented 2 years ago

Just from my general knowledge of Google OAuth tokens (not from replicating your specific experience) I can say there are multiple reasons for a refresh token to become invalid, i.e. you can't get access tokens with it anymore (which is I think what you're describing).

Note: Save refresh tokens in secure long-term storage and continue to use them as long as they remain valid. Limits apply to the number of refresh tokens that are issued per client-user combination, and per user across all clients, and these limits are different. If your application requests enough refresh tokens to go over one of the limits, older refresh tokens stop working.

https://developers.google.com/identity/protocols/oauth2

Once your refresh token becomes invalid, you can't get / refresh access tokens with it anymore and, yeah, you really do have to do the browser dance again.

If this shiny app is used with some reasonable frequency, then it's likely that whoever owns the token is getting (lots?) of other new tokens, causing the one stored for the app to fall off the end.

KryeKuzhinieri commented 2 years ago

Hey Jenny,

Thanks for the reply.

So, currently the app is on test mode and it appears that in the last week or so was not used. Could it be due to long inactivity? Also, it appears that the Oauth consent screen was on testing mode. Could this have caused the issue too?

jennybc commented 2 years ago

I don't think a single week of disuse is typically enough for a token to be deemed stale and made invalid.

I don't know re: whether testing mode has anything to do with modulating this.

How sure are you that the token is being found by your app? I suspect that you could be misinterpreting behaviour, i.e. the token file is not being found vs. it's being found and is non-refreshable.

KryeKuzhinieri commented 2 years ago

These are the logs I got from shiny-server.

2022-05-11T13:22:56.242690+00:00 : trying `token_fetch()`
2022-05-11T13:22:56.242860+00:00 : Error caught by `token_fetch()`:
2022-05-11T13:22:56.242781+00:00 : trying `credentials_service_account()`
2022-05-11T13:22:56.242917+00:00 : Argument 'txt' must be a JSON string, URL or file.
2022-05-11T13:22:56.242978+00:00 : trying `credentials_external_account()`
2022-05-11T13:22:56.243030+00:00 : aws.ec2metadata not installed; can't detect whether running on EC2 instance
2022-05-11T13:22:56.243089+00:00 : trying `credentials_app_default()`
2022-05-11T13:22:56.243145+00:00 : trying `credentials_gce()`
2022-05-11T13:22:57.247056+00:00 : trying `credentials_byo_oauth()`
2022-05-11T13:22:57.247127+00:00 : Error caught by `token_fetch()`:
2022-05-11T13:22:57.247289+00:00 : Gargle2.0 initialize
2022-05-11T13:22:57.247178+00:00 : inherits(token, "Token2.0") is not TRUE
2022-05-11T13:22:57.247228+00:00 : trying `credentials_user_oauth2()`
2022-05-11T13:22:57.247448+00:00 : matching token found in the cache
2022-05-11T13:22:57.247340+00:00 : adding "userinfo.email" scope
2022-05-11T13:22:57.247397+00:00 : loading token from the cache
2022-05-11T13:22:57.247574+00:00 : Warning: Unable to refresh token: invalid_grant
2022-05-11T13:22:57.247683+00:00 : Removing token from the cache:
2022-05-11T13:22:57.247499+00:00 : Auto-refreshing stale OAuth token.
2022-05-11T13:22:57.247849+00:00 :   Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
2022-05-11T13:22:57.247628+00:00 : • Token has been expired or revoked.
2022-05-11T13:22:57.247745+00:00 : 'secret'
2022-05-11T13:22:57.247798+00:00 : Warning: Error in gmailr_POST: Gmail API error: 401
2022-05-11T13:22:57.247896+00:00 : 
2022-05-11T13:22:57.248045+00:00 :   [No stack trace available]
KryeKuzhinieri commented 2 years ago

It states that the "Token has been expired or revoked"

jennybc commented 2 years ago

This looks like a good troubleshooting list:

https://blog.timekit.io/google-oauth-invalid-grant-nightmare-and-how-to-fix-it-9f4efaf1da35

I have definitely seen this one before: 1) Server clock/time is out of sync

KryeKuzhinieri commented 2 years ago

Thanks for the details. For now, now of these seems to be the case. I will keep testing with the current settings. If this there is a discovery in the future regarding this, I will update here again.

jennybc commented 2 years ago

BTW here's more about testing mode and its effect on token validity:

https://developers.google.com/identity/protocols/oauth2#webserver

A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.

nicocriscuolo commented 1 year ago

Hi everyone, I am also experiencing the problem of the token expiration because the "App" is in the testing phase. However, what is really not clear to me is what Google means when distinguishing between these stages of an application. My application has been developed in R through the shiny package, and deployed online through shinyapps.io, and several users are already using it.

I've explained my doubts a bit better on this GitHub post. Could you please give me some more explanations?

Thanks in advance for your help! I really appreciate it.