Electron桌面端Google OAuth2 PKCE认证流程中"invalid_grant (Missing code verifier)"错误求助
Electron桌面端Google OAuth2 PKCE认证流程中"invalid_grant (Missing code verifier)"错误求助
我正在为大学项目做一个网盘客户端,没太多相关经验,所以选择用Electron来开发——这样我可以像做网站一样写代码,但最终能得到一个桌面客户端。我希望它完全是客户端侧的,也就是说除了Google认证环节之外,不需要和其他Web服务器交互。
现在我在Google OAuth2.0的PKCE流程中遇到了问题,收到了以下错误:
GaxiosError: invalid_grant at Gaxios._request (MY_PROJECT_PATH\drive-encrypt_(Pre-production)\node_modules\gaxios\build\src\gaxios.js:142:23) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async OAuth2Client.getTokenAsync (MY_PROJECT_PATH\drive-encrypt_(Pre-production)\node_modules\google-auth-library\build\src\auth\oauth2client.js:158:21) at async file:///MY_PROJECT_PATH/drive-encrypt_(Pre-production)/index.js:154:35 { config: { retry: true, retryConfig: { httpMethodsToRetry: [Array], currentRetryAttempt: 0, retry: 3, noResponseRetries: 2, retryDelayMultiplier: 2, timeOfFirstRequest: 1743095188566, totalTimeout: 9007199254740991, maxRetryDelay: 9007199254740991, statusCodesToRetry: [Array] }, method: 'POST', url: 'https://oauth2.googleapis.com/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.14.0', 'x-goog-api-client': 'gl-node/20.10.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.com/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: 'Missing code verifier.' }, 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: 'Thu, 27 Mar 2025 17:06:28 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.com/token' } }, error: undefined, status: 400, [Symbol(gaxios-gaxios-error)]: '6.7.1' }
其中需要重点关注的部分是:
data: {
error: 'invalid_grant',
error_description: 'Missing code verifier.'
},
我已经尝试并检查了以下几点:
- 使用的是Google桌面客户端ID(因此只有默认的重定向URI)
- 已经用在线PKCE代码生成器验证了生成的code verifier和code challenge是匹配的
- 确认授权码只使用了一次,没有重复调用
以下是我相关的服务端代码(index.js):
import dotenv from "dotenv" dotenv.config() import fs from "fs" import path from "path" import { fileURLToPath } from "url" import { google } from "googleapis" import express from "express" import bodyParser from "body-parser" import mime from "mime-types" import multer from "multer" import cors from "cors" import crypto from "node:crypto" import { Readable } from "stream" const winregModule = await import('winreg'); const Registry = winregModule.default; import { generateAndStoreKEK, retrieveKEK, encryptData, decryptData } from "./frontend/scripts/keyManagement.js" import { recoverPassword, generateAndStorePasswordRecovery, recoveryDataExists } from './frontend/scripts/passwordRecovery.js'; import dns from "dns" import session from 'express-session'; const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const app = express() app.use(bodyParser.json()) app.use( cors({ origin: "http://localhost:8000", credentials: true, // Allow cookies to be sent with requests }), ) app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "lax", } })); const isDev = process.env.NODE_ENV === "development" const frontendDirectoryPath = isDev ? path.join(__dirname, "../frontend") : path.join(__dirname, "frontend") // Serve static files from the frontend directory app.use(express.static(frontendDirectoryPath)) // Serve login.html at the root URL app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "frontend/login.html")) }) const oauth2Client = new google.auth.OAuth2( process.env.CLIENT_ID, process.env.NODE_ENV === "production" ? "" : process.env.CLIENT_SECRET, "http://localhost:8000/google/redirect" ); let tokens try { const tokenData = fs.readFileSync(path.join(__dirname, "tokens.json"), "utf8") tokens = JSON.parse(tokenData) oauth2Client.setCredentials(tokens) } catch (err) { console.log("No tokens file found. User will need to authenticate.") } const PORT = process.env.PORT || 8000 const USER_PREFERENCES_FILE = path.join(__dirname, "userPreferences.json") function generateCodeVerifier() { return crypto.randomBytes(32).toString('base64url'); } function generateCodeChallenge(codeVerifier) { const hash = crypto.createHash("sha256").update(codeVerifier).digest("base64"); return hash .replace(/\+/g, "-") // Replace '+' with '-' .replace(/\//g, "_") // Replace '/' with '_' .replace(/=+$/, ""); // Remove '=' padding } app.get("/auth/google", (req, res) => { const pkceVerifier = generateCodeVerifier(); req.session.pkceCodeVerifier = pkceVerifier; // Store it in the session console.log("The pkce code verifier is: ", req.session.pkceCodeVerifier); const codeChallenge = generateCodeChallenge(pkceVerifier); console.log("Code Challenge:", codeChallenge); console.log("Code Verifier:", pkceVerifier); const authUrl = oauth2Client.generateAuthUrl({ access_type: "offline", scope: [ "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/contacts.readonly", "https://www.googleapis.com/auth/drive" ], code_challenge: codeChallenge, code_challenge_method: "S256", }); console.log("Generated Auth URL: ", authUrl); res.redirect(authUrl); }); app.get("/google/redirect", async (req, res) => { try { const { code } = req.query; const pkceVerifier = req.session.pkceCodeVerifier; console.log("PKCE Code Verifier in redirect: ", pkceVerifier); if (!pkceVerifier) { throw new Error("PKCE code verifier not found in session"); } console.log("Payload to getToken:", { code, code_verifier: pkceVerifier, }); const { tokens: newTokens } = await oauth2Client.getToken({ code, code_verifier: pkceVerifier, }); oauth2Client.setCredentials(newTokens) tokens = newTokens // Save tokens to file fs.writeFileSync(path.join(__dirname, "tokens.json"), JSON.stringify(newTokens)) // Set a cookie to indicate the user is authenticated res.cookie("authenticated", "true", { httpOnly: true, secure: true, sameSite: "Lax" }) req.session.pkceCodeVerifier = null; // Check if a password has been set in the registry const passwordSet = await isPasswordSet() console.log("Is password set?", passwordSet) if (!passwordSet) { console.log("Redirecting to password creation page") res.redirect("/password-creation.html") } else { console.log("Redirecting to password verification page") res.redirect("/password-verification.html") } } catch (err) { console.error("Error in /google/redirect:", err) res.redirect("/?error=" + encodeURIComponent("Authentication failed. Please try again.")) } })
抱歉代码格式可能不太规范,要是我犯了低级错误也请见谅。如果需要更多上下文或者其他信息,请随时告诉我,提前感谢大家的帮助!
备注:内容来源于stack exchange,提问作者UJWAL0747
相关产品推荐
相关产品推荐

