Node.js Google Calendar Passport OAuth 问题

问题描述 投票:0回答:1

我在 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 oauth-2.0 google-oauth
1个回答
0
投票

在这种情况下,最好隔离问题以进行详细检查

请按照以下步骤隔离问题,以便我们进一步调试

下面是一个最小的 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 设置或应用程序逻辑中的某个位置。

© www.soinside.com 2019 - 2024. All rights reserved.