You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GitHub OAuth API持续返回无效验证码问题排查求助

GitHub OAuth "bad verification code" 错误排查与修复方案

问题描述

学习API开发时,后端server.js中遇到GitHub OAuth问题:控制台持续返回"bad verification code(传入的验证码不正确或已过期)"错误。能正常从GitHub获取code,但用该验证码兑换access_token时触发错误。

此前在localhost:4000搭建的API搭配GitHub OAuth应用可正常工作:访问localhost:4000返回{"message":"Unauthorized"},通过localhost:4000/auth/github认证后可正常访问数据库数据。

后续搭建了运行在localhost:4001的前端应用,并创建新的GitHub OAuth应用,同时搭建运行在localhost:4002的后端服务,出现上述错误。已确认GitHub OAuth应用配置信息正确,调整/auth/github/callback路由逻辑后问题依旧。

代码片段

前端(app.js)

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState([]);
  const accessToken = process.env.SECRET_KEY

  useEffect(() => {
    if (accessToken) {
      fetchData();
    }
  }, [accessToken]);

async function fetchData() {
  try {
    // Use the obtained access token to fetch data from your API
    const response = await axios.get('http://localhost:4002/api/data', {
      headers: {
          Authorization: `Bearer ${accessToken}`,
      },
    });
    console.log("response")
    setData(response.data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

  return (
    <div>
      <h1>API Data</h1>
      <a href="http://localhost:4002/auth/github">Sign in</a>
      <ul>
        {data.map(item => (
          <li key={item._id}>
            {`${item.firstname} ${item.lastname}, Age: ${item.age}, Color: ${item.color}`}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

后端(server.js)

require('dotenv').config()

const express = require('express')
const axios = require('axios')
const cors = require('cors')
const passport = require('passport')
const GitHubStrategy = require('passport-github').Strategy
const http = require('http')
const bodyParser = require('body-parser');
const app = express()

// ... (other middleware and configurations)
console.log('Server file is being executed')

app.use(cors({ 
    origin: 'http://localhost:4001',
    credentials: true,
    optionsSuccessStatus: 200,
})) 

app.use(require('express-session')({
    secret: process.env.SECRET_KEY,
    resave: false,
    saveUninitialized: true,
}))

app.use(bodyParser.urlencoded({ 
    extended: false,
    limit: '10mb'
}));
app.use(bodyParser.json());
app.use(passport.initialize())
app.use(passport.session())

passport.use(new GitHubStrategy({
    clientID: process.env.NEW_CLIENT_ID,
    clientSecret: process.env.NEW_CLIENT_SECRET,
    callbackURL: "http://localhost:4002/auth/github/callback",
}, (accessToken, refreshToken, profile, done) => {
    return done(null, profile);
}));

passport.serializeUser((user, done) => {
    // Serialize user data into the session
    done(null, user);
});

passport.deserializeUser((obj, done) => {
    // Deserialize user data from the session
    done(null, obj);
});

app.get('/auth/github', passport.authenticate('github'), (req, res) => {
    console.log("just normal")
    // This callback function will be executed after successful authentication
    res.sendStatus(200); // You can send any response you want here
});

app.get('/auth/github/callback', 
    passport.authenticate('github', { failureRedirect: '/auth/github' }),
    async (req, res) => {
        try {
            const code = req.query.code

            const querystring = require('querystring')
            
            // Inside the callback route handler
            const data = querystring.stringify({
                client_id: process.env.NEW_CLIENT_ID,
                client_secret: process.env.NEW_CLIENT_SECRET,
                code: code,
                grant_type: 'authorization_code',
            })
            console.log("word:", data)

            const authOptions = {
                url: 'https://github.com/login/oauth/access_token',
                method: 'POST',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Authorization': 'Basic ' + Buffer.from(process.env.NEW_CLIENT_ID + ':' + process.env.NEW_CLIENT_SECRET).toString('base64'),
                }, 
                data: data
            }

            console.log('test', authOptions)
            console.log('gone:', authOptions.data)
            console.log('over here:', code)
            console.log('Sending HTTP request:');
            console.log('Method:', authOptions.method);
            console.log('URL:', authOptions.url);
            console.log('Headers:', authOptions.headers);
            console.log('Data:', authOptions.data);
            const response = await axios.post(authOptions.url, data, {
                headers: authOptions.headers,
                // data: authOptions.params
        });
            console.log('Received HTTP response:');
            console.log('Status:', response.status);
            console.log('Headers:', response.headers);
            console.log('Data:', response.data);
            console.log("mo:", response)
            console.log("response from Github:", response.data)
            if (response.data.access_token) {
                const accessToken = response.data.access_token
                console.log("Access Token:", accessToken)
                res.redirect('http://localhost:4001')
            } else {
                console.error("No access token received from Github.")
                res.status(500).json({ error: 'Internal Server Error here'})
            }
        } catch (error) {
            console.error('Error exchanging code for access token:', error);
            res.status(500).json({ error: 'Internal Server Error 2' });
        }
    }
)


function ensureAuthenticated(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.status(401).json({ message: 'Unauthorized' })
}

app.get('/protected-route', ensureAuthenticated, (req, res) => {
    // Route content for authenticated users
});

const server = http.createServer(app)

server.listen(4002, () => console.log('Server Started'));

问题根源分析

/auth/github/callback路由中存在重复操作:

  1. 调用passport.authenticate('github')时,passport-github已经自动完成了code兑换access_token的流程,且code一旦被使用就会失效。
  2. 后续手动获取req.query.code再次向GitHub请求兑换,此时code已被passport消耗,自然返回"bad verification code"错误。

另外,GitHubStrategy的回调函数中已经直接提供了accessToken参数,完全不需要手动重复兑换流程。

修复方案

核心修改点

删除/auth/github/callback路由中手动兑换code的所有逻辑,直接利用passport认证后得到的用户信息和accessToken。如果需要将accessToken传递给前端,可通过重定向参数(生产环境建议用session或安全方式传递,避免暴露token)或后端接口返回。

修改后的后端回调路由代码

app.get('/auth/github/callback', 
    passport.authenticate('github', { failureRedirect: '/auth/github' }),
    (req, res) => {
        // passport认证成功后,req.user包含用户信息
        console.log('Authenticated user:', req.user);
        
        // 重定向到前端并携带accessToken(示例,生产环境不建议直接在URL传token)
        res.redirect(`http://localhost:4001?access_token=${req.user.accessToken}`);
    }
)

配套修改GitHubStrategy(用于保存accessToken)

passport.use(new GitHubStrategy({
    clientID: process.env.NEW_CLIENT_ID,
    clientSecret: process.env.NEW_CLIENT_SECRET,
    callbackURL: "http://localhost:4002/auth/github/callback",
}, (accessToken, refreshToken, profile, done) => {
    // 将accessToken挂载到profile对象,方便后续调用
    profile.accessToken = accessToken;
    return done(null, profile);
}));

额外优化建议

  • 前端app.js中accessToken = process.env.SECRET_KEY逻辑错误,SECRET_KEY是后端session密钥,并非用户的access_token。应从后端重定向的URL参数中获取,或通过接口请求获取。
  • 生产环境中,禁止在URL中直接传递access_token,建议用HttpOnly Cookie存储,或后端提供接口让前端携带session凭证获取token。

内容的提问来源于stack exchange,提问作者Matt Ly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 02:05:55