admin管理员组

文章数量:1410737

I'm experimenting with Google OAuth2.0 authorization and access token refreshing in a Firebase cloud function. In my Tauri desktop app I'm using Firebase's signInWithPopup() function in order to retrieve access token, refresh token and Id token.

const provider = new GoogleAuthProvider();
provider.addScope('')
        
const result = await signInWithPopup(auth, provider);

const credentials = GoogleAuthProvider.credentialFromResult(result)

const uid = result.user.uid
const idToken = await result.user.getIdToken()
const accessToken = credentials?.accessToken
const refreshToken = result.user.refreshToken

Then I'm calling my Firebase Google Cloud Function, where I'm sending the ID token and refresh token. In this function I would like to refresh the access token, since it can be expired:

const response = await fetch('', {
  method: 'POST',
  headers: {'Content-Type':'application/json'},
  body: JSON.stringify({ data: { uid, idToken, refreshToken }}),
});

The Firebase cloud funtion looks like this:

export const refreshAccessToken = onRequest({ cors: "*",  }, async (req, res) => {
  try {
    const { idToken, refreshToken } = req.body.data;
 
    const refreshedAccessToken = await refreshGoogleAccessToken(refreshToken);
    
    res.json({ refreshedAccessToken });
  } catch (e) {
    throw new HttpsError("unknown", 'Error', e)
  }
})

async function refreshGoogleAccessToken(refreshToken: string) {
  const oauth2Client = new google.auth.OAuth2(
      googleOauth.clientId,
      googleOauth.clientSecret,
      ''
  );
  oauth2Client.setCredentials({ refresh_token: refreshToken, scope: `` });
  
  logger.debug("credentials - ok")

  // this does not work
  const refreshed = await oauth2Client.refreshAccessToken();

  logger.debug("refreshed - ok", refreshed )
  
  return refreshed.credentials.access_token;
}

The problem is that oauth2Client.refreshAccessToken() fails. In cloud functions log I can see only "credentials - ok" log, but not "refreshed - ok", plus there is an error:

GaxiosError: invalid_grant
    at Gaxios._request (/workspace/node_modules/gaxios/build/src/gaxios.js:142:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async OAuth2Client.refreshTokenNoCache (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:212:19)
    at async OAuth2Client.refreshAccessTokenAsync (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:247:19)
    at async OAuth2Client.getAccessTokenAsync (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:276:23)
    at async refreshGoogleAccessToken (/workspace/lib/index.js:61:21)
    at async /workspace/lib/index.js:36:38 {
  config: {
    retry: true,
    retryConfig: {
      httpMethodsToRetry: [Array],
      currentRetryAttempt: 0,
      retry: 3,
      noResponseRetries: 2,
      retryDelayMultiplier: 2,
      timeOfFirstRequest: 1741544322630,
      totalTimeout: 9007199254740991,
      maxRetryDelay: 9007199254740991,
      statusCodesToRetry: [Array]
    },
    method: 'POST',
    url: '',
    data: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'google-api-nodejs-client/9.15.1',
      'x-goog-api-client': 'gl-node/22.14.0'
    },
    paramsSerializer: [Function: paramsSerializer],
    body: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
    validateStatus: [Function: validateStatus],
    responseType: 'unknown',
    errorRedactor: [Function: defaultErrorRedactor]
  },
  response: {
    config: {
      retry: true,
      retryConfig: [Object],
      method: 'POST',
      url: '',
      data: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
      headers: [Object],
      paramsSerializer: [Function: paramsSerializer],
      body: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
      validateStatus: [Function: validateStatus],
      responseType: 'unknown',
      errorRedactor: [Function: defaultErrorRedactor]
    },
    data: { error: 'invalid_grant', error_description: 'Bad Request' },
    headers: {
      'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
      'cache-control': 'no-cache, no-store, max-age=0, must-revalidate',
      'content-encoding': 'gzip',
      'content-type': 'application/json; charset=utf-8',
      date: 'Sun, 09 Mar 2025 18:18:42 GMT',
      expires: 'Mon, 01 Jan 1990 00:00:00 GMT',
      pragma: 'no-cache',
      server: 'scaffolding on HTTPServer2',
      'transfer-encoding': 'chunked',
      vary: 'Origin, X-Origin, Referer',
      'x-content-type-options': 'nosniff',
      'x-frame-options': 'SAMEORIGIN',
      'x-xss-protection': '0'
    },
    status: 400,
    statusText: 'Bad Request',
    request: { responseURL: '' }
  },
  error: undefined,
  status: 400,
  [Symbol(gaxios-gaxios-error)]: '6.7.1'
}

What is wrong here? How could I refresh the access token? Should I use googleapis (gapi) lib for this?


functions/package.json:

"firebase-admin": "^13.2.0",
"firebase-functions": "^6.3.1",
"googleapis": "^146.0.0"

NOTE: As far as I understand, in this case proper way should be to use signInWithRedirect() instead of signInWithPopup() and set the redirect to another cloud function, where I would retrieve the refresh token, store it securely to db and then retrieve it from the db when calling the refreshAccessToken function instead of sending it in the call from Tauri directly. But for experimnetal reasons this should be fine and should not prevent access token refreshing..

I'm experimenting with Google OAuth2.0 authorization and access token refreshing in a Firebase cloud function. In my Tauri desktop app I'm using Firebase's signInWithPopup() function in order to retrieve access token, refresh token and Id token.

const provider = new GoogleAuthProvider();
provider.addScope('https://www.googleapis/auth/calendar')
        
const result = await signInWithPopup(auth, provider);

const credentials = GoogleAuthProvider.credentialFromResult(result)

const uid = result.user.uid
const idToken = await result.user.getIdToken()
const accessToken = credentials?.accessToken
const refreshToken = result.user.refreshToken

Then I'm calling my Firebase Google Cloud Function, where I'm sending the ID token and refresh token. In this function I would like to refresh the access token, since it can be expired:

const response = await fetch('https://us-central1-axel-86.cloudfunctions/refreshAccessToken', {
  method: 'POST',
  headers: {'Content-Type':'application/json'},
  body: JSON.stringify({ data: { uid, idToken, refreshToken }}),
});

The Firebase cloud funtion looks like this:

export const refreshAccessToken = onRequest({ cors: "*",  }, async (req, res) => {
  try {
    const { idToken, refreshToken } = req.body.data;
 
    const refreshedAccessToken = await refreshGoogleAccessToken(refreshToken);
    
    res.json({ refreshedAccessToken });
  } catch (e) {
    throw new HttpsError("unknown", 'Error', e)
  }
})

async function refreshGoogleAccessToken(refreshToken: string) {
  const oauth2Client = new google.auth.OAuth2(
      googleOauth.clientId,
      googleOauth.clientSecret,
      ''
  );
  oauth2Client.setCredentials({ refresh_token: refreshToken, scope: `https://www.googleapis/auth/calendar` });
  
  logger.debug("credentials - ok")

  // this does not work
  const refreshed = await oauth2Client.refreshAccessToken();

  logger.debug("refreshed - ok", refreshed )
  
  return refreshed.credentials.access_token;
}

The problem is that oauth2Client.refreshAccessToken() fails. In cloud functions log I can see only "credentials - ok" log, but not "refreshed - ok", plus there is an error:

GaxiosError: invalid_grant
    at Gaxios._request (/workspace/node_modules/gaxios/build/src/gaxios.js:142:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async OAuth2Client.refreshTokenNoCache (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:212:19)
    at async OAuth2Client.refreshAccessTokenAsync (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:247:19)
    at async OAuth2Client.getAccessTokenAsync (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:276:23)
    at async refreshGoogleAccessToken (/workspace/lib/index.js:61:21)
    at async /workspace/lib/index.js:36:38 {
  config: {
    retry: true,
    retryConfig: {
      httpMethodsToRetry: [Array],
      currentRetryAttempt: 0,
      retry: 3,
      noResponseRetries: 2,
      retryDelayMultiplier: 2,
      timeOfFirstRequest: 1741544322630,
      totalTimeout: 9007199254740991,
      maxRetryDelay: 9007199254740991,
      statusCodesToRetry: [Array]
    },
    method: 'POST',
    url: 'https://oauth2.googleapis/token',
    data: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'google-api-nodejs-client/9.15.1',
      'x-goog-api-client': 'gl-node/22.14.0'
    },
    paramsSerializer: [Function: paramsSerializer],
    body: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
    validateStatus: [Function: validateStatus],
    responseType: 'unknown',
    errorRedactor: [Function: defaultErrorRedactor]
  },
  response: {
    config: {
      retry: true,
      retryConfig: [Object],
      method: 'POST',
      url: 'https://oauth2.googleapis/token',
      data: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
      headers: [Object],
      paramsSerializer: [Function: paramsSerializer],
      body: '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.',
      validateStatus: [Function: validateStatus],
      responseType: 'unknown',
      errorRedactor: [Function: defaultErrorRedactor]
    },
    data: { error: 'invalid_grant', error_description: 'Bad Request' },
    headers: {
      'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
      'cache-control': 'no-cache, no-store, max-age=0, must-revalidate',
      'content-encoding': 'gzip',
      'content-type': 'application/json; charset=utf-8',
      date: 'Sun, 09 Mar 2025 18:18:42 GMT',
      expires: 'Mon, 01 Jan 1990 00:00:00 GMT',
      pragma: 'no-cache',
      server: 'scaffolding on HTTPServer2',
      'transfer-encoding': 'chunked',
      vary: 'Origin, X-Origin, Referer',
      'x-content-type-options': 'nosniff',
      'x-frame-options': 'SAMEORIGIN',
      'x-xss-protection': '0'
    },
    status: 400,
    statusText: 'Bad Request',
    request: { responseURL: 'https://oauth2.googleapis/token' }
  },
  error: undefined,
  status: 400,
  [Symbol(gaxios-gaxios-error)]: '6.7.1'
}

What is wrong here? How could I refresh the access token? Should I use googleapis (gapi) lib for this?


functions/package.json:

"firebase-admin": "^13.2.0",
"firebase-functions": "^6.3.1",
"googleapis": "^146.0.0"

NOTE: As far as I understand, in this case proper way should be to use signInWithRedirect() instead of signInWithPopup() and set the redirect to another cloud function, where I would retrieve the refresh token, store it securely to db and then retrieve it from the db when calling the refreshAccessToken function instead of sending it in the call from Tauri directly. But for experimnetal reasons this should be fine and should not prevent access token refreshing..

Share Improve this question edited Mar 10 at 18:15 Axel Productions 86 asked Mar 9 at 15:47 Axel Productions 86Axel Productions 86 1,5884 gold badges23 silver badges43 bronze badges 7
  • Try catching, logging and throwing the error from oauth2Client.refreshAccessToken() to see what it logs. So async function refreshGoogleAccessToken(refreshToken: string) { try { const refreshed = await oauth2Client.refreshAccessToken(); return refreshed.credentials.access_token; }catch(err){ console.log('oauth2Client.refreshAccessToken err:', err); throw err; } }; Obviously include the rest of your code in the try block. – jQueeny Commented Mar 9 at 17:14
  • @jQueeny The same stupid error. Perhaps it is something with node.. – Axel Productions 86 Commented Mar 9 at 17:37
  • The error is too generic. Can you log the error inside the catch before HttpsError so console.log('refreshGoogleAccessToken err:', e); throw new HttpsError("unknown", 'Error', e) – jQueeny Commented Mar 9 at 18:07
  • @jQueeny aha, it is showing invalid_grant now.. check updated post with whole error – Axel Productions 86 Commented Mar 9 at 18:27
  • There could be a host of reasons why you're getting invalid_grant. Clearly something wrong with the refreshToken assigned to refresh_token in the setCredentials but not sure why. – jQueeny Commented Mar 9 at 18:40
 |  Show 2 more comments

2 Answers 2

Reset to default 0

The issue occurs because oauth2Client.refreshAccessToken() is deprecated. Instead, use oauth2Client.getAccessToken() to refresh the token correctly.

import { google } from 'googleapis';

async function refreshGoogleAccessToken(refreshToken: string) {
  const oauth2Client = new google.auth.OAuth2(
      googleOauth.clientId,
      googleOauth.clientSecret,
      ''
  );
  oauth2Client.setCredentials({ refresh_token: refreshToken });

  try {
    const refreshed = await oauth2Client.getAccessToken();
    if (!refreshed.token) throw new Error("Failed to refresh access token.");
    
    console.log("refreshed - ok", refreshed.token);
    return refreshed.token;
  } catch (error) {
    console.error("Error refreshing token:", error);
    throw new Error("Token refresh failed");
  }
}

I don't know what's the issue with access token refreshing in my example, but in the end I solved it by not using Firebase at all and instead of refreshing token in my cloud function I'm doing it on a local server opened from Tauri by using this plugin:

https://github/FabianLars/tauri-plugin-oauth/tree/v2

This plugin allows to open local server on specified port either from JS part, or from Rust part of the Tauri code and provides callback for handling the OAuth redirect. In my case I'm doing it from JS (not sure if it is secure enough).

import {
  start,
  cancel,
  onInvalidUrl,
  onUrl,
} from "@fabianlars/tauri-plugin-oauth";

const googleOauth = {
  clientId: '***.apps.googleusercontent',
  clientSecret: '***',
  redirectUri: 'http://127.0.0.1:8899',
  authUrl: 'https://accounts.google/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis/token'
}

function logIn() {
  startServer();

  const authUrl =
    `${googleOauth.authUrl}?` +
    `client_id=${googleOauth.clientId}&` +
    `redirect_uri=${googleOauth.redirectUri}&` +
    `access_type=offline&` +
    `prompt=consent&` +
    `response_type=code&` +
    `scope=https://www.googleapis/auth/calendar openid email profile`;

  openUrl(authUrl);
}

async function startServer() {
  const port = await start({ ports: [8899] });

  // process OAuth redirect in this callback
  const unlistenUrl = await onUrl(async (url: string) => {
    console.log("Received OAuth URL:", url);

    const urlObj = new URL(url);
    const params = new URLSearchParams(urlObj.search);

    const code = params.get("code");
    if (!code) {
      console.log("Not authenticated!", params);
      return;
    }

    // exchange authorization code for tokens
    const tokens = await exchangeTokens(code);

    // if needed, refresh access token
    setTimeout(async () => {
      await refreshAccessToken((tokens as any).refresh_token);
    }, 2000);
  });

  const unlistenInvalidUrl = await onInvalidUrl((error: any) => {
    console.error("Received invalid OAuth URL:", error);
  });

  // Store unlisten functions to call them when stopping the server
  (window as any).unlistenFunctions = [unlistenUrl, unlistenInvalidUrl];
}

async function exchangeTokens(authCode: string) => {
  const params = new URLSearchParams({
    code: authCode,
    client_id: googleOauth.clientId,
    client_secret: googleOauth.clientSecret,
    redirect_uri: googleOauth.redirectUri,
    grant_type: "authorization_code",
  });

  return await fetch(googleOauth.tokenUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: params.toString(),
  })
    .then((response) => response.json())
    .then((data) => {
      console.log("tokens", data);
      return data;
    })
    .catch((error) => console.error(error));
};

async function refreshAccessToken(refreshToken: string) => {
  const params = new URLSearchParams({
    client_id: googleOauth.clientId,
    client_secret: googleOauth.clientSecret,
    refresh_token: refreshToken,
    grant_type: 'refresh_token'
  });

  return await fetch(googleOauth.tokenUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: params.toString()
  })
  .then(response => response.json())
  .then(data => {
    const accessToken = data.access_token;
    const expiresIn = data.expires_in;
  
    console.log('New access token:', accessToken, " - expires in:", expiresIn);
    
    return data;
  })
  .catch(error => console.error('Error refreshing access token:', error));

In google cloud console should be the local redirect url with port specified:

Authorised redirect URIs:

http://127.0.0.1:8899

本文标签: nodejs(Tauri) Can39t refresh access token in cloud functioninvalidgrantStack Overflow