admin管理员组

文章数量:1357577

I am trying to make a drive client for my college project and I am not very experienced in it. That's why I am building the application using electron so that I can develop the application as if it was a website, but it will work as a Desktop Client. I want it to be completely client-side which means no interaction with any web server other that google for authentication purposes.

Right now, I am facing an issue with the PKCE flow in Google OAuth2.0 I am getting the following error:

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: '',
    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: '',
      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: '' }
  },
  error: undefined,
  status: 400,
  [Symbol(gaxios-gaxios-error)]: '6.7.1'
}

The main part to focus here is:-

data: { error: 'invalid_grant', error_description: 'Missing code verifier.' },

I have tried and checked the following things:

  1. I am using Google's Desktop Client ID (So, I only have the default redirect URI)
  2. I have also verified that the generated code verifier matches the code challenge using online PKCE Code Generator
  3. I have also checked that the authorization code is only used once, and never repeated again.

This the my relevant server code (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: [
      ".profile",
      ".email",
      ".readonly",
      ";
    ],
    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."))
  }
})

Sorry in advance for the poor code formatting and if I am doing something extremely stupid here.

Let me know if you need anything else for context. And Thanks in advance

本文标签: javascriptIssue in Google OAuth flow when using PKCEStack Overflow