Retrieving Google access tokens from Auth0 client side
After moving from Google implicit grant authorization to code model, I saw there were a bunch of things that were still left to do when it comes to user management. I want to be able to control which users log in, monitor problems properly, etc.
This is when I decided to explore Auth0 which offers a decent free plan.
🔧 Initial setup
Initial configuration with Auth0 is really smooth, their docs are amazing and it just felt awesome compared to integrating directly with Google Oauth using their docs.
In our case, we are going with client side auth and thus, we are choosing the path of using @auth0/auth0-react
. Once you've created your Social login with Google/Auth0, you can set your provider in your root as follows:
<Auth0Provider
domain="<your_auth0_domain>"
clientId="<your_auth0_client_id>"
authorizationParams={{
redirect_uri: 'http://localhost:3000',
scope: 'https://www.googleapis.com/auth/drive.file', # adapt accordingly
}}
>
{children}
</Auth0Provider>
This works easily, first attempt which is a lot of saying from where we are coming from. When you log in, it creates the user in Auth0 with all the information you expect it to. You can inspect the user profile and raw information and see it has pulled the information from your Google profile.
🤷 Where is my access token?
One of the things I quickly realized was that I didn't see any access_token
in my user profile. After searching a bit, you discover that the token is indeed there but you have to retrieve it through the management API as this information is found under identities
object and you need the read:user_idp_tokens
scope.
What's most annoying is that there is no official supported way to retrieve IDP access tokens in your frontend. Their docs tell you to create a backend which.. I don't want to so I decided to hack my way through this.
🥷 The hack
I don't understand why it's so complicated to have access to a Google access token when, if you integrate directly with Google Oauth2, the access token is readily available and accessed from the frontend.
After playing a lot with the different configuration options from Auth0, I managed to implement a workaround that allows me to retrieve it in the frontend with no backend.
🤯 Custom social provider
The first step is to find a way to expose the access_token
somewhere that can be then accessed by standard ways for Auth0 to retrieve custom data. The only way I found to do this is by writing a custom social provider.
The reason is because the "Fetch User Profile Script" has access to the access token returned by the social provider after logging in. To go with this step then, you just need to create a new custom social provider with the same exact parameters except the profile script which should be:
function(accessToken, ctx, cb) {
const info = JSON.parse(atob(ctx.id_token.split('.')[1]));
const profile = {
user_id: info.sub,
email: info.email,
name: info.name,
picture: info.picture,
email_verified: info.email_verified,
user_metadata: {
access_token: accessToken,
},
};
// Call OAuth2 API with the accessToken and create the profile
cb(null, profile);
}
Basically, what we are doing here is set the accessToken
received by the function as user_metadata
. This accessToken
is the Social provider one, not the Auth0 one!
Once you've done this, if you test this connection, you will see that the access_token
attribute is filled in the user metadata.
Note that, in order to use the new connection, you have to specify it in the Auth0Provider
:
<Auth0Provider
domain="<your_auth0_domain>"
clientId="<your_auth0_client_id>"
authorizationParams={{
redirect_uri: 'http://localhost:3000',
scope: 'https://www.googleapis.com/auth/drive.file', # adapt accordingly
connection: <custom_connection_name>,
}}
>
{children}
</Auth0Provider>
🔌 Exposing user metadata in the frontend
🍏 Via custom claims
This is a documented approach for when you want to expose custom attributes in the User object that Auth0 returns.
It is the simplest and what I would recommend as it consists only on adding a custom Action to the login flow with the following code:
exports.onExecutePostLogin = async (event, api) => {
const { access_token } = event.user.user_metadata;
if (event.authorization) {
api.idToken.setCustomClaim('accessToken', access_token);
}
};
which is quite self explanatory. It means, add a new attribute accessToken
to the id_token
with the value of user_metadata.access_token
. In the frontend you will be able to then do:
const { user } = useAuth0();
console.log(user.accessToken);
🍎 Via user management API
I don't recommend this approach but leaving it here for documentation purposes in case someone (or me) needs to do some more complicated stuff using the management API from the frontend.
Next step is to access user metadata in the frontend. To do so, we need to issue a request to user management
API. We can issue these requests using the access token that Auth0 returns as described in their docs. As mentioned, the requests sent from the frontend using this token have very limited scope (i.e. read current user, modify metadata, etc).
First, need to modify the provider again by adding the audience (the management API) and the scope so we can read the user:
<Auth0Provider
domain="<your_auth0_domain>"
clientId="<your_auth0_client_id>"
authorizationParams={{
audience: 'https://<your_auth0_domain>/api/v2/',
redirect_uri: 'http://localhost:3000',
scope: 'read:current_user https://www.googleapis.com/auth/drive.file', # adapt accordingly
connection: <custom_connection_name>,
}}
>
{children}
</Auth0Provider>
Next, we need to retrieve the Auth0 token and with it, retrieve the google access token:
const { user, isAuthenticated, getAccessTokenSilently } = useAuth0();
const [accessToken, setAccessToken] = React.useState('');
/**
* If we have an authenticated user, retrieve the Google access token
* using the management API from Auth0. Note we are super hacky here as
* we store the access token from Google in the user metadata of Auth0
* so we can access it.
*/
React.useEffect(() => {
async function load(u: User) {
const managementToken = await getAccessTokenSilently(); # This returns the Auth0 access_token
const userDetailsByIdUrl = `https://<your_auth0_domain>/api/v2/users/${u.sub}`;
const metadataResponse = await fetch(userDetailsByIdUrl, {
headers: {
Authorization: `Bearer ${managementToken}`,
},
});
const resp = await metadataResponse.json();
setAccessToken(resp.user_metadata.access_token); # This is an access token that can be used by GAPI
}
if (user && isAuthenticated) {
load(user);
}
}, [user, isAuthenticated, getAccessTokenSilently, setAccessToken]);
And that's it, once this is done, you can set the gapi access token to what the hook sets in accessToken
and queries will work
for whatever scopes you requested when authorizing using the custom provider.
😵 Refresh
Access tokens emitted by Google Oauth only last for 1 hour which means, after one hour, your app will break when trying to access those resources. In order to keep the app working, you need to refresh the access_token
using a refresh_token
. Refresh tokens are returned by Google when you pass access_type
as offline
when authorizing. However, we can't repeat the same approach as with the access_token
because the refresh token is not available in the "Fetch User Profile Script".
Currently, as documented by Auth0 docs it's not possible to refresh tokens without a backend and I couldn't find a workaround as I did with access tokens. For this case, what I decided to do is make the session with Auth0 to be of one hour so users need to sign in again after one hour so the IDP access token is refreshed.
💚 Conclusion
I don't understand why it's so difficult to retrieve IDP access tokens from the frontend when direct integrations with the social providers do just that, return an access token that you can just use.
Anyway, below is a list of interesting resources/docs I used when implementing this
- https://auth0.com/docs/quickstart/spa/react/02-calling-an-api
- https://auth0.com/docs/authenticate/identity-providers/calling-an-external-idp-api#from-the-frontend
- https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims#add-custom-claims-to-a-token
- https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/
And as always, here you can find the PR with all the changes for shipping this to Maffin.