使用Passport和OAuth 2.0对接Whoop API时登录回调报错求助
Whoop OAuth2 + Passport 认证错误排查与修复方案
核心问题梳理
以下是代码中导致回调端点认证失败的关键问题:
- 未定义的
err变量触发ReferenceError accessToken字段拼写错误(写成acessToken)- 缺失Passport必需的
serializeUser/deserializeUser配置 - 未处理Whoop API请求的HTTP错误
- 异步数据库操作未捕获异常
- 中间件顺序错误导致CORS/session失效
修复后的完整代码
路由文件(./routes/user.js)
const express = require('express') const app = express() const passport = require('passport') const OAuth2Strategy = require('passport-oauth2') const User = require('../Models/User') require('dotenv').config() const whoopOAuthConfig = { authorizationURL: `${process.env.WHOOP_API_HOSTNAME}/oauth/oauth2/auth`, tokenURL: `${process.env.WHOOP_API_HOSTNAME}/oauth/oauth2/token`, clientID: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, callbackURL: process.env.CALLBACK_URL, state: true, scope: [ 'offline', 'read:profile', 'read:recovery', 'read:cycles', 'read:sleep', 'read:workout', 'read:body_measurement' ], } async function getUser(accessToken, refreshToken, { expires_in }, profile, done) { try { console.log(profile) const { first_name, last_name, user_id } = profile let user = await User.findOne({ userId: user_id }) if (user) { // 修正字段拼写错误 user.accessToken = accessToken user.refreshToken = refreshToken user.expiresIn = Date.now() + expires_in * 1000 user.firstName = first_name user.lastName = last_name user.userId = user_id await user.save() } else { user = new User({ accessToken: accessToken, refreshToken: refreshToken, expiresIn: Date.now() + expires_in * 1000, firstName: first_name, lastName: last_name, userId: user_id, }) await user.save() // 新增新用户保存操作 } done(null, user) } catch (err) { done(err) } } async function fetchProfile(accessToken, done) { try { console.log(accessToken) const profileResponse = await fetch( `${process.env.WHOOP_API_HOSTNAME}/developer/v1/user/profile/basic`, { headers: { Authorization: `Bearer ${accessToken}`, }, }, ) // 处理API请求失败场景 if (!profileResponse.ok) { throw new Error(`Whoop API请求失败: ${profileResponse.statusText}`) } const profile = await profileResponse.json() done(null, profile) } catch (err) { done(err) } } const whoopAuthorizationStrategy = new OAuth2Strategy(whoopOAuthConfig, getUser) whoopAuthorizationStrategy.userProfile = fetchProfile passport.use('oauth2', whoopAuthorizationStrategy) // 新增Passport序列化/反序列化配置,支持session持久化 passport.serializeUser((user, done) => { done(null, user.id) }) passport.deserializeUser(async (id, done) => { try { const user = await User.findById(id) done(null, user) } catch (err) { done(err) } }) var printSometing = function(req, res, next) { console.log("I'm still here"); return next(); } app.get('/auth/performance', passport.authenticate('oauth2')); app.get('/auth/performance/callback', passport.authenticate('oauth2', { failureRedirect: '/', failureMessage: true }), printSometing, function (req, res) { res.redirect('http://localhost:3000/'); }); module.exports = app;
主文件(index.js)
const express = require('express') const app = express() const port = 8081 const cors = require('cors') const session = require('express-session') const mongoose = require('mongoose') require('dotenv').config() var bodyParser = require('body-parser'); const passport = require('passport') mongoose.connect(process.env.MONGO_URI) const database = mongoose.connection; database.on('error', console.error.bind(console, 'connection error:')); database.once('connected', function() { console.log("Connected to MongoDB") }); app.get('/', (req, res) => { res.send({msg: 'Hello World!'}) }) // 调整中间件顺序:CORS需在session/Passport之前加载 app.use(cors({ origin: 'http://localhost:3000', // 指定前端域名,提升安全性 credentials: true // 允许携带session cookie })) app.use(session({ secret: process.env.CLIENT_SECRET, resave: false, // 关闭不必要的session刷新 saveUninitialized: false, // 符合隐私规范,仅在认证后创建session cookie: { secure: process.env.NODE_ENV === 'production', // 生产环境强制HTTPS maxAge: 24 * 60 * 60 * 1000 // session有效期1天 } })) app.use(express.urlencoded({ extended: false })); app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); app.use(passport.initialize()); app.use(passport.session()); app.use('/api', require('./routes/user.js')); app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
额外检查项
- 确认Whoop开发者后台的回调URL与代码中
CALLBACK_URL完全一致(包括HTTP/HTTPS、端口、路径) - 检查
.env文件中WHOOP_API_HOSTNAME是否为生产环境地址:https://api.prod.whoop.com - 确保User模型中的字段名与代码一致(如
accessToken而非acessToken) - 生产环境必须启用HTTPS,否则session cookie的
secure属性无法生效,会导致认证失败
内容的提问来源于stack exchange,提问作者Pere Tarrida Domingo
相关产品推荐
相关产品推荐

