Google OAuth2

Google OAuth2 client for Node.js.

Google OAuth2
import axios from 'axios';
import qs from 'qs';
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '@/config';
import { createGoogleAxiosResponseInterceptor } from '@/modules/verificationTracker/utils/googleAxiosResponseInterceptor';
import { UserRoleEnum } from '@/enums';

type GoogleTokensResult = {
  access_token: string;
  expires_in: number;
  refresh_token: string;
  scope: string;
  id_token: string;
};

type GoogleUserResult = {
  id: string;
  email: string;
  verified_email: boolean;
  name: string;
  given_name: string;
  family_name: string;
  picture: string;
  locale: string;
};

type GoogleRefreshAccessTokenResult = {
  access_token: string;
  expires_in: number;
  scope: string;
  token_type: string;
  id_token: string;
};

const googleClient = axios.create();

class Google {
  static getAuthToken = async (
    code: string,
    redirectUri: string,
  ): Promise<GoogleTokensResult> => {
    const url = 'https://oauth2.googleapis.com/token';

    const values = {
      code,
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      redirect_uri: redirectUri,
      grant_type: 'authorization_code',
    };

    const res = await axios.post<GoogleTokensResult>(
      url,
      qs.stringify(values),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      },
    );
    return res.data;
  };

  static getUser = async ({
    idToken,
    accessToken,
  }: {
    idToken: string;
    accessToken: string;
  }): Promise<GoogleUserResult> => {
    const url = `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${accessToken}`;
    const res = await axios.get<GoogleUserResult>(url, {
      headers: {
        Authorization: `Bearer ${idToken}`,
      },
    });
    return res.data;
  };

  static refreshAccessToken = async (
    refreshToken: string,
  ): Promise<GoogleRefreshAccessTokenResult> => {
    const url = 'https://oauth2.googleapis.com/token';
    const values = {
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      refresh_token: refreshToken,
      grant_type: 'refresh_token',
    };
    const res = await axios.post<GoogleRefreshAccessTokenResult>(
      url,
      qs.stringify(values),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      },
    );
    return res.data;
  };

  static revokeToken = async (
    accessToken: string,
    user: {id: string, role: UserRoleEnum},
  ): Promise<void> => {
    const url = `https://oauth2.googleapis.com/revoke?token=${accessToken}`;

    createGoogleAxiosResponseInterceptor(googleClient, user);

    const res = await googleClient.post(url, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });

    return res.data;
  };
}

export default Google;

When a request fails due to expired access token, you can use the intercept the response, refresh the token and retry the request.

import { AxiosInstance } from 'axios';
import { refreshGoogleAccountAccessToken } from '@/util';
import { UserRoleEnum } from '@/enums';

export const createGoogleAxiosResponseInterceptor = (
  client: AxiosInstance,
  user: { id: string; role: UserRoleEnum }
): void => {
  const interceptor = client.interceptors.response.use(
    (response) => response,
    (error) => {
      // Reject promise if usual error
      if (!error.isAxiosError || !error.response) return Promise.reject(error);
      if (error.response.status !== 401) {
        return Promise.reject(error);
      }

      /*
       * When response code is 401, try to refresh the token.
       * Eject the interceptor so it doesn't loop in case
       * token refresh fails with 401.
       */
      client.interceptors.response.eject(interceptor);

      return refreshGoogleAccountAccessToken(user)
        .then(({ accessToken }) => {
          error.response.config.headers.Authorization = `Bearer ${accessToken}`;
          return client(error.response.config);
        })
        .catch((_error: Error) => Promise.reject(_error))
        .finally(() => createGoogleAxiosResponseInterceptor(client, user));
    }
  );
};