我在 Node.js 应用程序中使用 Google 日历进行身份验证时遇到问题。
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;
在这种情况下,最好隔离问题以进行详细检查
请按照以下步骤隔离问题,以便我们进一步调试
下面是一个最小的 Node.js 脚本,它使用 Google OAuth 2.0 机制来验证和获取用户的个人资料信息。
首先,确保您已安装所需的软件包。您可以使用 npm 安装它们:
npm install google-auth-library open express dotenv
然后,创建一个新文件(例如,
google-oauth-test.js
)并将以下代码粘贴到该文件中:
require('dotenv').config();
const { google } = require('google-auth-library');
const open = require('open');
const express = require('express');
const app = express();
const port = 3000; // Ensure this port matches the one in your GOOGLE_REDIRECT_URL
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URL = process.env.GOOGLE_REDIRECT_URL;
const oauth2Client = new google.auth.OAuth2(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_REDIRECT_URL
);
// Generate a url that asks permissions for the Profile and Email scopes
const scopes = [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
];
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
prompt: 'consent' // Forces a consent screen every time. Remove this if not needed.
});
// Open an http server to accept the oauth callback. In this simple example, the
// entire OAuth2 flow is being handled within a single route.
app.get('/auth/google/callback', async (req, res) => {
const { tokens } = await oauth2Client.getToken(req.query.code);
oauth2Client.setCredentials(tokens);
const oauth2 = google.oauth2({
auth: oauth2Client,
version: 'v2'
});
oauth2.userinfo.get(
function(err, response) {
if (err) {
console.log(err);
return res.status(400).send(err);
}
console.log(response.data); // The user's profile information
res.send(response.data);
}
);
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
// Automatically open the browser to the consent page
open(url);
});
运行脚本之前,请确保使用您的 Google 客户端 ID、密码和重定向 URI 填写您的
.env
文件(或环境变量):
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
GOOGLE_REDIRECT_URL=your_google_redirect_uri_here
最后,使用 Node.js 运行脚本:
node google-oauth-test.js
此脚本将启动本地服务器并打开一个浏览器窗口,您可以在其中完成 OAuth 流程。同意后,Google 用户个人资料信息应记录在控制台中,如果一切配置正确,OAuth 流程应成功完成。这可以帮助确定问题是否出在您的凭据、Google 设置或应用程序逻辑中的某个位置。