Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieving Google access tokens from Auth0 client side #259

Open
argaen opened this issue Feb 6, 2024 · 0 comments
Open

Retrieving Google access tokens from Auth0 client side #259

argaen opened this issue Feb 6, 2024 · 0 comments

Comments

@argaen
Copy link
Contributor

argaen commented Feb 6, 2024

+++
slug = "auth0-google-access-token"
reading_time = "10m"
summary = "A guide to retrieve Google access tokens from Auth0 in the frontend"
+++

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.

Screenshot 2024-02-06 at 10 48 02 PM

💚 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

And as always, here you can find the PR with all the changes for shipping this to Maffin.

@argaen argaen changed the title [draft] Retrieving Google access tokens from Auth0 client side Retrieving Google access tokens from Auth0 client side Feb 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant