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..
2 Answers
Reset to default 0The 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
版权声明:本文标题:node.js - (Tauri) Can't refresh access token in cloud function - invalid_grant - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744867053a2629405.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
oauth2Client.refreshAccessToken()
to see what it logs. Soasync 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:14catch
beforeHttpsError
soconsole.log('refreshGoogleAccessToken err:', e); throw new HttpsError("unknown", 'Error', e)
– jQueeny Commented Mar 9 at 18:07invalid_grant
now.. check updated post with whole error – Axel Productions 86 Commented Mar 9 at 18:27invalid_grant
. Clearly something wrong with therefreshToken
assigned torefresh_token
in thesetCredentials
but not sure why. – jQueeny Commented Mar 9 at 18:40