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

使用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}`)
})

额外检查项

  1. 确认Whoop开发者后台的回调URL与代码中CALLBACK_URL完全一致(包括HTTP/HTTPS、端口、路径)
  2. 检查.env文件中WHOOP_API_HOSTNAME是否为生产环境地址:https://api.prod.whoop.com
  3. 确保User模型中的字段名与代码一致(如accessToken而非acessToken)
  4. 生产环境必须启用HTTPS,否则session cookie的secure属性无法生效,会导致认证失败

内容的提问来源于stack exchange,提问作者Pere Tarrida Domingo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 21:07:06