in Code, Tutorials

Refreshing Google API Access Tokens in Auth0 Apps

tl;dr just show me the tutorial!

As I’ve written before, I love using Auth0 to provide authentication-as-a-service in the apps that I build. One of the things I love about it is how easy it is to set it up to use 3rd party identity providers (e.g. Google, GitHub, etc.). Not only does it make the signup/login process smooth and secure for the people who use my apps, but it also streamlines the process of getting users’ permission to integrate more deeply with these other services.

aurora logo
The Aurora Project Logo

For example, I’m currently working on an app to create a coding environment called The Aurora Project where my students can log in and write code to control LED light shows that will play in my office. The code itself will be stored on GitHub. I set up Auth0 to use GitHub as the 3rd party identity provider, and linked my app directly to GitHub. Now when students log in, they’ll be prompted to give Aurora permission to get files from and save files to their GitHub accounts. My partner asked me why I just didn’t have my students write their code directly on GitHub as well. Yeah, that would probably be smarter, but this way is more fun, and since my students are novice programmers, I can constrain the environment in certain ways to make it simpler and easier for them.

Anyway, integrating an app using Auth0 with GitHub is pretty easy because by default GitHub issues API tokens that don’t expire. I can associate each student’s token with their account and whenever I need to get files from or save files to their account, I can just use that API token.

Portphilio Logo

Google doesn’t make things quite so easy. I’m working on another project called Portphilio that will allow people to create online portfolios of their work. It’s targeted mostly at my students, and my goal is to guide them in the thoughtful curation of collections of their work that will help them do things like get good jobs. The artifacts in their portfolios will come from a number of sources. One of those sources will be their own personal Google Drive folders. So, I’m allowing people to use their Google accounts to both log into the app, and at the same time, I’m getting their permission to access their Drive folders. That means they’ll be able to open up a Google Drive file picker from directly within the app and select a file to highlight in one of their portfolios.

Auth0 makes it fairly straightforward to implement this. I can proxy a request for a user’s connection info through my backend API (built with FeathersJs). Using the Auth0 Management API I can request a version of the user’s profile that contains an access_token for their Google account. After that it’s pretty straightforward to follow the docs for incorporating a Google file picker into a page. Except: Google access tokens expire after only one hour! That means that after only an hour of being logged into Portphilio (which allows you to stay logged in indefinitely), if they try to open the file picker, they see this:

Oh no!!! Even though I’m still logged in to Portphilio, my Google API access token has expired!

When what they’re supposed to see is this:

No problem! I’ll just refresh the Google access_token and then I can refresh the content in the file picker. Easier said than done. Other than completely logging out of Portphilio (Auth0) and then logging in again (not a fun thing to do in the middle of your workflow), there is no easy and straightforward way to update a Google API access_token via the Auth0 Management API. It’s taken me several days of digging through the docs, various forums, GitHub issue threads, and an unhealthy amount of trial and error, but I think I’ve finally figured it out.

If you know of a better way to do this, please let me know!

How to Refresh a Google API Token

I’ll walk you through the process. My apps are written with JavaScript (VueJs frontend, FeathersJs backend) so code examples will use JS and libraries common to JS project. You could, of course, implement a similar solution in most any language, with any framework.

The Last Bit First

The goal of all of this is to get a new Google API access_token. This is done via a simple HTTP request and is documented here. Here’s an example of a function to do it in a JS app using the Axios HTTP library:

The values for GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET can be hard-coded into your app (or more likely environment variables for your app as pictured here). You can get these values from the Google Developer Console for your app. Usefully, the id_token that is returned (in JWT format) contains an iat (issued at) claim and an exp (expires at) claim–timestamps that provide the exact expiration time for the new token so you don’t have to guesstimate based on the expires_in value that is also attached. You can decode the JWT using a library like jwt-decode.

For security reasons you should never host and run a function like this one in an SPA, PWA, or other client-side code. Your Google client ID is super secret and sensitive, and you should therefore always make sure that this function is hosted and run on a secure server. I created a REST endpoint to do this in my FeathersJs API backend.

Refreshing the access_token is the easy part, though. The hard part is getting the refresh_token in the first place.

Getting the Refresh Token from Auth0

It took me a long time to figure out how to get a refresh_token from Auth0. Even though there is documentation here and here, a GitHub thread about it here, and Auth0 Community forum threads about it here, here, and here, I still found it confusing. Based on others’ comments, I think they found it confusing as well. I’ll discuss why I think this is at the end, but first, here’s the steps to do it yourself.

Step 1: Configure Auth0.js WebAuth Correctly

My app is an SPA/PWA built with VueJs. I’m using Auth0’s hosted login pages, which is what they recommend for this kind of app. In my app I have a service/provider for Auth0-related functionality (full code here). I initialize the WebAuth instance as follows:

This is pretty similar to the Quickstarts for SPAs on Auth0’s site. The main difference is the addition of the accessType: 'offline' property to the config object that gets passed to the authorize() function. The docs that specify you need that property are here and here.

Step 2: Capture the refresh_token with a Rule

There are a few important things to know here:

  1. People MUST be logging in with their Google account
  2. You must have your Auth0 app set up to request the necessary privileges from the Google user BEFORE they sign up
  3. You only get ONE chance to do this when people first sign up for an account!

The reason you only get one chance to do this is that Google only sends a refresh_token back for authorization attempts where the user explicitly gives permission to an app to access some resources in their Google account. In other words, you only get a refresh_token when the user logging in sees and gives consent on a screen like this:

A standard consent form giving a 3rd party app permission to access resources in a Google account.

After the user has given consent, and the consent form is no longer displayed (i.e. on the 2nd and subsequent logins) Google does not send a refresh_token along with the access_token it sends for normal logins. That means you have to capture it on the first login. The most reliable way to do this is with an Auth0 rule like this:

After this, the user’s app_metadata property should look something like:

Now you can retrieve the refresh_token from your backend API server (I use FeathersJs) and make a request for a new access_token as described above. Since a refresh_token essentially gives permanent access to a Google account, it is highly sensitive and should never be sent to a web client (like an SPA, or PWA) where it might get stored in the browser and become accessible to unauthorized agents. To prevent it from accidentally being sent to a client, you should add one more rule.

Step 3: Sanitize User Info with Another Rule

You can prevent the refresh_token from being passed to web clients by adding another rule to your Auth0 app like this:

Make sure that this rule comes AFTER the rule we wrote above in Step 2. This rule will prevent web clients from being able to retrieve/read the refresh_token but still make it available for getUser() requests made from the Auth0 Management API.

Step 4: (Optional) Rule to Send refresh_token to API

Another possibility you may want to consider is instead of or in addition to storing the refresh_token in app_metadata, you may want to send it to your backend API to be stored for use there. Refresh tokens typically do not expire unless and until they are blacklisted for some reason. Blacklisting typically only happens if the Google account is suspected of being hacked or compromised, or if too many refresh tokens have been issued. There is a limit to the number that can exist, although I don’t know what that is. As such, long-term storage of a refresh_token makes sense. Here’s one way to do that. In another Auth0 rule:

Note: this rule is not bulletproof. It will fail in case the user already exists in the backend API. It needs to be rewritten to update if the attempt to add fails. Also note that in Auth0 rules we can use 3rd party NPM packages. The full list of available packages is here.

The main reason I say this rule is “optional” is that there are plenty of scenarios in which it could fail. It should not be the only way you have to store the refresh_token. I strongly suggest always adding it to app_metadata.

Concluding Thoughts

This strikes me as a pretty common use case for using Google APIs along with Auth0 in apps. I spent a long time banging my head on this problem, so I wanted to write it down so I wouldn’t forget.

One problem I have yet to solve is how to know when the Google access_token has expired or is about to expire. Although we know that it is valid for 3600 seconds (one hour), Auth0 doesn’t send us the id_token that it presumably came with so we don’t know exactly when that hour begins or ends. Overusing a refresh_token is one way to get it blacklisted, so the solution can’t be just to always refresh the access_token before using it. I suppose that since the id_token passed with a standard Auth0 login contains an iat (issued at) claim, we could calculate the first access_token expiration as one hour from that. Subsequent access_token refresh requests will contain their own id_token and iat value, so for those we’ll know exactly if and when they will expire.

I said I would comment on why I think this was such a confusing process. One reason is that both Auth0 and Google use OAuth2 for authentication. Using Google via Auth0 is like an OAuth2 process nested inside of another OAuth2 process. As such, there are multiple, separate access_tokens, refresh_tokens, id_tokens, etc. Also the method for obtaining a refresh_token is NOT standard across OAuth2 identity providers. That makes it pretty extraordinary that Auth0 even supports this use case at all, and understandable that the documentation, while pretty clear, takes a while before you can really wrap your head around it. It also means that the community of people who have faced this problem (and solved it!) is pretty small.

So, if you’re reading this, you’re probably one of that very small circle of people who really needed to know how to do this. I hope it was helpful! I am not sensitive about my code, so if you think I made any mistakes, big or small, or if you know of some way to do it better, please comment or contact me and I’ll update this post. Thanks!!!

Bonus: 2nd Chance!

So earlier when I said that you only get ONE chance to capture the refresh_token, I wasn’t being completely forthcoming. There IS a way to get Google to send you another refresh_token but it comes at a cost for users. By adding approvalPrompt: 'force' to the list of options sent with the call to authorize() you can force your users to have to go through the Google consent process again. This is kind of a hassle and might actually be alarming to some of your users, especially if they know that they’ve already given consent and are wondering why they’re being asked to consent again. It just feels fishy. That being said, it may be your only option if you’re trying to capture the refresh_token from users in an app that has existed for a while already.

Here’s the code to force the consent process to be triggered (note: this REPLACES the LAST line from STEP 1 above):

It would be nice if you could somehow know BEFORE they log in whether or not this is their first time and could somehow conditionally decide whether or not to force them back through the consent process. If I figure out how to do that, I’ll be sure to write another blog post.

Write a Comment

Comment