Node.js中Google Calendar Passport OAuth认证失败问题
Node.js 对接 Google Calendar 认证失败排查
问题概述
在Node.js应用中对接Google Calendar时遭遇Invalid Credentials认证错误,已反复核对GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET和GOOGLE_REDIRECT_URL配置,授权重定向URI也经过多次校验,但本地及生产环境均出现相同问题。
错误日志
2024-03-06 12:51:09 info: Deserializing user with ID: 2024-03-06 12:51:09 info: User deserialized successfully: 2024-03-06 12:51:09 info: State parameter from Google: 2024-03-06 12:51:09 info: Entered /auth/google/callback route. Processing... 2024-03-06 12:51:09 info: Initiating Passport authentication... 2024-03-06 12:51:11 info: Passport authentication processed. 2024-03-06 12:51:11 error: Passport authentication error: 2024-03-06 12:51:11 error: Invalid Credentials 2024-03-06 12:51:11 error: Error occurred on route /google-calendar/auth/google/callback - Method: GET
相关代码
const express = require("express"); const passport = require("passport"); const GoogleStrategy = require("passport-google-oauth20").Strategy; const router = express.Router(); const User = require("../models/user"); const { google } = require("googleapis"); const Email = require("../models/email"); const { getCalendarEvent, createGoogleCalendarEvent, } = require("../calendarEvents"); const logger = require("../logger"); const logInfo = logger.logInfo; const logError = logger.logError; // Setup Passport passport.use( new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_REDIRECT_URL, }, async function (accessToken, refreshToken, profile, done) { logInfo("Entered GoogleStrategy callback"); // Add detailed logging for received tokens and profile logInfo("Received Access Token: ", accessToken); logInfo("Received Refresh Token: ", refreshToken); logInfo("Received Profile: ", profile); // Check if accessToken is a non-empty string if (typeof accessToken !== "string" || !accessToken) { logError("Access token is either missing or not a string."); return done(new Error("No access token received"), null); } // Check if refreshToken is a non-empty string if (typeof refreshToken !== "string" || !refreshToken) { logError("Refresh token is either missing or not a string."); return done(new Error("No refresh token received"), null); } let user; try { user = await User.findOneAndUpdate( { googleId: profile.id }, { googleId: profile.id }, { upsert: true, new: true }, ); } catch (err) { logError("Error querying the database for user:", err); return done(err); } if (!user) { logError("Unexpected error: No user returned after findOneAndUpdate."); return done( new Error("Unexpected error during user retrieval or creation"), null, ); } logInfo("User retrieved or created successfully with ID:", user._id); // Before saving tokens, refresh if needed const validAccessToken = await checkAndRefreshToken( user, accessToken, refreshToken, ); accessToken = validAccessToken || accessToken; // Save the tokens to the user try { await user.setGoogleTokens({ accessToken, refreshToken }); logInfo( "Access and refresh tokens saved successfully for user:", user._id, ); } catch (err) { logError("Error while saving tokens for user:", err); } logInfo("Exiting GoogleStrategy callback"); done(null, user); }, ), ); async function checkAndRefreshToken( user, currentAccessToken, currentRefreshToken, ) { try { const currentTime = Date.now() / 1000; const EXPIRY_WINDOW = 300; // 5 minutes if ( !user.googleTokens || !user.googleTokens.expiryDate || currentTime > user.googleTokens.expiryDate - EXPIRY_WINDOW ) { const oauth2Client = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URL, ); // Add detailed logging logInfo("Refreshing Google access token..."); oauth2Client.setCredentials({ refresh_token: currentRefreshToken }); const newAccessToken = await oauth2Client.getAccessToken(); if (!newAccessToken || !newAccessToken.token) { throw new Error("Failed to refresh the access token"); } return newAccessToken.token; } return currentAccessToken; } catch (err) { logError("Error in token refresh:", err); throw err; } } passport.serializeUser((user, done) => { logInfo("Serializing user with ID:", user.id); done(null, user.id); }); passport.deserializeUser(async (id, done) => { logInfo("Deserializing user with ID:", id); try { const user = await User.findById(id); if (user) { logInfo("User deserialized successfully:", user._id); done(null, user); } else { logError("Failed to deserialize user with ID:", id); done(new Error("Failed to deserialize user"), null); } } catch (err) { logError("Error during deserialization:", err); done(err, null); } }); router.get("/", function (req, res) { res.render("google-calendar"); }); // The route to redirect the user to the Google consent screen router.get( "/connect", passport.authenticate("google", { scope: [ "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events", ], accessType: "offline", prompt: "consent", }), ); // The route where Google redirects the user after the consent screen router.get("/auth/google/callback", function (req, res, next) { // Log the state parameter, which can be helpful to trace back any CSRF issues logInfo("State parameter from Google:", req.query.state); // Initial Entry Point logInfo("Entered /auth/google/callback route. Processing..."); // Before Passport Authentication logInfo("Initiating Passport authentication..."); passport.authenticate("google", function (err, user, info) { // Enhanced logging if (info) { logInfo("Additional Passport Info:", JSON.stringify(info, null, 2)); } // Log after passport authentication has processed logInfo("Passport authentication processed."); if (err) { // 3. Inside the Authentication Callback - Error Scenario logError("Passport authentication error:", { errorMessage: err.message, errorStack: err.stack, additionalInfo: info, error: err, }); return next(err); } if (!user) { // User not found scenario logWarn("No user data retrieved during Passport authentication."); return res.redirect("/login"); } // Successful authentication logInfo("User authenticated successfully:", { userId: user.id, username: user.username, }); req.logIn(user, function (err) { if (err) { logError( "Error during the login phase after Passport authentication:", err, ); return next(err); } return res.redirect("/users/" + user.username); }); })(req, res, next); }); module.exports = router;
排查与解决方案
1. 验证环境变量加载
在passport.use初始化前添加日志,确认环境变量是否正确加载:
console.log("Loaded Client ID:", process.env.GOOGLE_CLIENT_ID?.slice(0, 10) + "..."); console.log("Loaded Redirect URL:", process.env.GOOGLE_REDIRECT_URL);
若输出为空或不符,检查.env文件格式、部署环境变量配置,确保变量未被遗漏或覆盖。
2. 核对Google Cloud控制台配置
- 确认Google Calendar API已启用:进入控制台搜索并启用该API。
- 检查OAuth客户端类型:本地开发用“桌面应用”,生产环境用“Web应用”。
- 重定向URI完全匹配:需包含协议(http/https)、域名、端口和完整路径,比如本地为
http://localhost:3000/google-calendar/auth/google/callback,生产环境对应实际域名。 - 确认客户端ID/密钥无格式错误:避免复制时带入多余空格或特殊字符。
3. 修复Token刷新逻辑问题
首次认证时,新用户无历史token记录,当前代码会错误进入刷新分支,导致无效请求。修改checkAndRefreshToken函数:
async function checkAndRefreshToken(user, currentAccessToken, currentRefreshToken) { try { // 新用户直接返回当前token,无需刷新 if (!user.googleTokens) { return currentAccessToken; } const currentTime = Date.now() / 1000; const EXPIRY_WINDOW = 300; if (!user.googleTokens.expiryDate || currentTime > user.googleTokens.expiryDate - EXPIRY_WINDOW) { const oauth2Client = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URL, ); oauth2Client.setCredentials({ refresh_token: currentRefreshToken }); const newAccessToken = await oauth2Client.getAccessToken(); if (!newAccessToken?.token) { throw new Error("Failed to refresh the access token"); } return newAccessToken.token; } return currentAccessToken; } catch (err) { logError("Error in token refresh:", err); throw err; } }
同时,首次保存token时需手动添加过期时间(Google access token默认有效期3600秒):
// 在保存token前添加 const expiryDate = Date.now() + 3600 * 1000; await user.setGoogleTokens({ accessToken, refreshToken, expiryDate });
4. 清除旧授权缓存
- 登录Google账号,在“安全”->“第三方应用访问权限”中移除当前应用的授权记录。
- 本地开发时清除浏览器缓存和Cookie,重新发起授权流程。
5. 增强日志排查
在Passport回调中补充打印info参数,获取Google返回的具体错误详情:
passport.authenticate("google", function (err, user, info) { if (info) { logInfo("Google Auth Detailed Info:", JSON.stringify(info)); } // 原有逻辑 });
内容的提问来源于stack exchange,提问作者Jordan
相关产品推荐
相关产品推荐

